package com.baiyue.lastleg.appointmentplanning; import org.junit.jupiter.api.Test; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.List; import static org.junit.jupiter.api.Assertions.*; class AppointmentPlanningPolicyTest { private static Instant at(int hour) { return Instant.parse("2026-10-15T" + String.format("%02d", hour) + ":00:00Z"); } private static AppointmentPlanningPolicy.Cargo cargo(String source, String customer, String origin, String destination, Integer packages, Instant from, Instant to) { return new AppointmentPlanningPolicy.Cargo(source, customer, origin, destination, "US", "US", packages, from, to, true, null); } @Test void twoCustomersCanPlanOneDestinationWithoutAPlanNumberDriverOrExternalConfirmation() { var result = AppointmentPlanningPolicy.group(List.of( cargo("ORDER:1", "A", "WH-1", "FBA:ONT8", 10, at(1), at(5)), cargo("BOOKING:B-1", "B", "WH-1", "FBA:ONT8", 8, at(3), at(8)))); assertEquals(18, result.packages()); assertEquals(2, result.customerCount()); assertEquals(at(3), result.readyFrom()); assertEquals(at(5), result.readyTo()); assertFalse(result.readinessIncomplete()); } @Test void unknownReadinessIsPreservedInsteadOfInventingAnArrivalOffset() { var result = AppointmentPlanningPolicy.group(List.of(cargo("ORDER:1", "A", "WH-1", "FBA:ONT8", 10, null, null))); assertTrue(result.readinessIncomplete()); assertNull(result.readyFrom()); assertNull(result.readyTo()); } @Test void differentOriginsDestinationsAndDisjointWindowsAreNotSilentlyMerged() { var first = cargo("ORDER:1", "A", "WH-1", "FBA:ONT8", 10, at(1), at(3)); assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of(first, cargo("ORDER:2", "B", "WH-2", "FBA:ONT8", 8, at(1), at(3))))); assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of(first, cargo("ORDER:2", "B", "WH-1", "FBA:LAX9", 8, at(1), at(3))))); assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of(first, cargo("ORDER:2", "B", "WH-1", "FBA:ONT8", 8, at(5), at(8))))); } @Test void sourceIdentityMustNotBeCountedAgainAfterMaterialization() { var first = cargo("BOOKING:B-1", "A", "WH-1", "FBA:ONT8", 10, null, null); assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of(first, first))); var unknown=AppointmentPlanningPolicy.group(List.of(cargo("ORDER:1", "A", "WH-1", "FBA:ONT8", null, null, null))); assertNull(unknown.packages());assertEquals(1,unknown.unknownQuantitySources()); } @Test void nonUsAndBlockedSourcesCannotBeTreatedAsEligibleAmericanCargo() { assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of( new AppointmentPlanningPolicy.Cargo("ORDER:1", "A", "WH-1", "FBA:YYZ", "US", "CA", 1, null, null, true, null)))); var error = assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.group(List.of( new AppointmentPlanningPolicy.Cargo("ORDER:1", "A", "WH-1", "FBA:ONT8", "US", "US", 1, null, null, false, "货物已被有效安排占用")))); assertEquals("货物已被有效安排占用", error.getMessage()); } @Test void changedReadinessIdentifiesImpactWithoutOverwritingTheConfirmedTime() { var before = new AppointmentPlanningPolicy.Forecast("ORDER:1", "1", at(1), at(3)); var after = new AppointmentPlanningPolicy.Forecast("ORDER:1", "2", at(4), at(8)); var impact = AppointmentPlanningPolicy.compare(before, after, at(6)); assertTrue(impact.reviewRequired()); assertEquals("READINESS_AFTER_APPOINTMENT", impact.reason()); assertEquals(at(6), impact.confirmedAt()); assertEquals(at(1), impact.previousEta()); assertEquals(at(4), impact.latestEta()); assertFalse(AppointmentPlanningPolicy.compare(after, after, at(6)).reviewRequired()); } @Test void lostForecastNeedsReviewAndUnrelatedSourcesCannotBeCompared() { var before = new AppointmentPlanningPolicy.Forecast("ORDER:1", "1", at(1), at(3)); assertEquals("FORECAST_MISSING", AppointmentPlanningPolicy.compare(before, new AppointmentPlanningPolicy.Forecast("ORDER:1", "2", null, null), at(6)).reason()); assertThrows(IllegalArgumentException.class, () -> AppointmentPlanningPolicy.compare(before, new AppointmentPlanningPolicy.Forecast("ORDER:2", "2", at(2), at(3)), at(6))); } @Test void losAngelesTimesResolveToTheLocationZoneAndKeepAnUnknownEndUnknown() { var result = AppointmentTimePolicy.resolve(LocalDateTime.parse("2026-10-15T09:00:00"), null, "America/Los_Angeles", null, null); assertEquals(Instant.parse("2026-10-15T16:00:00Z"), result.start()); assertNull(result.end()); assertThrows(IllegalArgumentException.class, () -> AppointmentTimePolicy.resolve(LocalDateTime.parse("2026-10-15T09:00:00"), null, null, null, null)); assertThrows(IllegalArgumentException.class, () -> AppointmentTimePolicy.resolve(LocalDateTime.parse("2026-10-15T09:00:00"), null, "UTC-08:00", null, null)); assertEquals(Instant.parse("2026-11-01T08:30:00Z"),AppointmentTimePolicy.resolve(LocalDateTime.parse("2026-11-01T01:30:00"),null,"America/Phoenix",null,null).start()); } @Test void daylightSavingGapIsRejectedAndOverlapRequiresAnExplicitValidOffset() { assertThrows(IllegalArgumentException.class, () -> AppointmentTimePolicy.resolve(LocalDateTime.parse("2026-03-08T02:30:00"), null, "America/Los_Angeles", null, null)); var overlap = LocalDateTime.parse("2026-11-01T01:30:00"); assertThrows(IllegalArgumentException.class, () -> AppointmentTimePolicy.resolve(overlap, null, "America/Los_Angeles", null, null)); assertEquals(Instant.parse("2026-11-01T09:30:00Z"), AppointmentTimePolicy.resolve(overlap, null, "America/Los_Angeles", ZoneOffset.ofHours(-8), null).start()); assertThrows(IllegalArgumentException.class, () -> AppointmentTimePolicy.resolve(overlap, null, "America/Los_Angeles", ZoneOffset.ofHours(8), null)); } }