EverWrath/src/test/server/game/Events/HolidayDateCalculatorTest.cpp
blinkysc 10b4f04c44
fix(Core/Events): Fix multi-stage holiday events ending early on restart (#25032)
Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
Co-authored-by: sudlud <sudlud@users.noreply.github.com>
2026-03-10 20:47:17 -03:00

1391 lines
53 KiB
C++

/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "HolidayDateCalculator.h"
#include "gtest/gtest.h"
class HolidayDateCalculatorTest : public ::testing::Test
{
protected:
void ExpectDate(const std::tm& date, int year, int month, int day)
{
EXPECT_EQ(date.tm_year + 1900, year);
EXPECT_EQ(date.tm_mon + 1, month);
EXPECT_EQ(date.tm_mday, day);
}
// Helper to verify a date is a valid calendar date
bool IsValidDate(int year, int month, int day)
{
if (month < 1 || month > 12) return false;
if (day < 1 || day > 31) return false;
// Check days in month
int daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// Leap year check for February
if (month == 2)
{
bool isLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
if (isLeap) daysInMonth[2] = 29;
}
return day <= daysInMonth[month];
}
// Helper to check if a year is a leap year
bool IsLeapYear(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
};
// ============================================================
// Easter Calculation Tests (1900-2200)
// ============================================================
TEST_F(HolidayDateCalculatorTest, EasterSunday_KnownDates)
{
// Verify against known Easter dates
// Source: https://www.census.gov/data/software/x13as/genhol/easter-dates.html
struct EasterTestCase { int year; int month; int day; };
std::vector<EasterTestCase> testCases = {
// Historical dates
{ 1900, 4, 15 },
{ 1901, 4, 7 },
{ 1950, 4, 9 },
{ 1999, 4, 4 },
// Recent dates
{ 2000, 4, 23 },
{ 2010, 4, 4 },
{ 2020, 4, 12 },
{ 2021, 4, 4 },
{ 2022, 4, 17 },
{ 2023, 4, 9 },
{ 2024, 3, 31 },
{ 2025, 4, 20 },
{ 2026, 4, 5 },
{ 2027, 3, 28 },
{ 2028, 4, 16 },
{ 2029, 4, 1 },
{ 2030, 4, 21 },
// Future dates
{ 2050, 4, 10 },
{ 2100, 3, 28 },
{ 2150, 4, 12 },
{ 2200, 4, 6 },
};
for (auto const& tc : testCases)
{
std::tm easter = HolidayDateCalculator::CalculateEasterSunday(tc.year);
SCOPED_TRACE("Year: " + std::to_string(tc.year));
ExpectDate(easter, tc.year, tc.month, tc.day);
}
}
TEST_F(HolidayDateCalculatorTest, EasterSunday_ValidDateRange_1900_2200)
{
// Easter must always fall between March 22 and April 25 (inclusive)
for (int year = 1900; year <= 2200; ++year)
{
std::tm easter = HolidayDateCalculator::CalculateEasterSunday(year);
SCOPED_TRACE("Year: " + std::to_string(year));
// Verify year is correct
EXPECT_EQ(easter.tm_year + 1900, year);
// Easter must be in March or April
EXPECT_TRUE(easter.tm_mon == 2 || easter.tm_mon == 3)
<< "Easter must be in March (2) or April (3), got month " << easter.tm_mon;
// Easter range: March 22 - April 25
if (easter.tm_mon == 2) // March
{
EXPECT_GE(easter.tm_mday, 22) << "Easter in March must be >= 22";
EXPECT_LE(easter.tm_mday, 31) << "Easter in March must be <= 31";
}
else // April
{
EXPECT_GE(easter.tm_mday, 1) << "Easter in April must be >= 1";
EXPECT_LE(easter.tm_mday, 25) << "Easter in April must be <= 25";
}
// Easter must be a Sunday
EXPECT_EQ(easter.tm_wday, 0) << "Easter must be a Sunday";
}
}
TEST_F(HolidayDateCalculatorTest, EasterSunday_AlwaysSunday_1900_2200)
{
// Verify Easter is always on Sunday for entire range
for (int year = 1900; year <= 2200; ++year)
{
std::tm easter = HolidayDateCalculator::CalculateEasterSunday(year);
EXPECT_EQ(easter.tm_wday, 0) << "Easter " << year << " should be Sunday";
}
}
// ============================================================
// Nth Weekday Calculation Tests (1900-2200)
// ============================================================
TEST_F(HolidayDateCalculatorTest, NthWeekday_Thanksgiving_1900_2200)
{
// Verify 4th Thursday of November for all years
for (int year = 1900; year <= 2200; ++year)
{
std::tm date = HolidayDateCalculator::CalculateNthWeekday(year, 11, Weekday::THURSDAY, 4);
SCOPED_TRACE("Year: " + std::to_string(year));
// Must be in November
EXPECT_EQ(date.tm_mon + 1, 11);
// Must be a Thursday
EXPECT_EQ(date.tm_wday, static_cast<int>(Weekday::THURSDAY));
// 4th Thursday must be between 22nd and 28th
EXPECT_GE(date.tm_mday, 22);
EXPECT_LE(date.tm_mday, 28);
// Verify it's actually the 4th occurrence
// First Thursday can be 1-7, so 4th is (first + 21) which gives range 22-28
int firstThursday = date.tm_mday - 21;
EXPECT_GE(firstThursday, 1);
EXPECT_LE(firstThursday, 7);
}
}
TEST_F(HolidayDateCalculatorTest, NthWeekday_AllWeekdays_1900_2200)
{
// Test first occurrence of each weekday in January for all years
for (int year = 1900; year <= 2200; ++year)
{
for (int weekday = 0; weekday <= 6; ++weekday)
{
std::tm date = HolidayDateCalculator::CalculateNthWeekday(year, 1, static_cast<Weekday>(weekday), 1);
SCOPED_TRACE("Year: " + std::to_string(year) + " Weekday: " + std::to_string(weekday));
// Must be in January
EXPECT_EQ(date.tm_mon + 1, 1);
// Must be the correct weekday
EXPECT_EQ(date.tm_wday, weekday);
// First occurrence must be within first 7 days
EXPECT_GE(date.tm_mday, 1);
EXPECT_LE(date.tm_mday, 7);
}
}
}
TEST_F(HolidayDateCalculatorTest, NthWeekday_SecondThirdFourth_Validation)
{
// Verify 2nd, 3rd, 4th occurrences are exactly 7 days apart
for (int year = 2000; year <= 2100; ++year)
{
for (int month = 1; month <= 12; ++month)
{
std::tm first = HolidayDateCalculator::CalculateNthWeekday(year, month, Weekday::MONDAY, 1);
std::tm second = HolidayDateCalculator::CalculateNthWeekday(year, month, Weekday::MONDAY, 2);
std::tm third = HolidayDateCalculator::CalculateNthWeekday(year, month, Weekday::MONDAY, 3);
std::tm fourth = HolidayDateCalculator::CalculateNthWeekday(year, month, Weekday::MONDAY, 4);
SCOPED_TRACE("Year: " + std::to_string(year) + " Month: " + std::to_string(month));
EXPECT_EQ(second.tm_mday - first.tm_mday, 7);
EXPECT_EQ(third.tm_mday - second.tm_mday, 7);
EXPECT_EQ(fourth.tm_mday - third.tm_mday, 7);
}
}
}
// ============================================================
// Date Packing/Unpacking Tests
// Note: Pack format uses 5 bits for year offset from 2000, so range is 2000-2031
// ============================================================
TEST_F(HolidayDateCalculatorTest, PackDate_RoundTrip)
{
// Test that pack/unpack preserves date information correctly
struct PackTestCase { int year; int month; int day; };
std::vector<PackTestCase> testCases = {
{ 2000, 1, 1 }, // Min year in pack range
{ 2000, 12, 31 }, // End of min year
{ 2015, 6, 15 }, // Mid range
{ 2020, 1, 24 }, // Lunar Festival 2020
{ 2025, 11, 27 }, // Thanksgiving 2025
{ 2030, 4, 28 }, // Noblegarden 2030
{ 2031, 12, 31 }, // Max year in pack range
};
for (auto const& tc : testCases)
{
std::tm date = {};
date.tm_year = tc.year - 1900;
date.tm_mon = tc.month - 1;
date.tm_mday = tc.day;
mktime(&date);
uint32_t packed = HolidayDateCalculator::PackDate(date);
std::tm unpacked = HolidayDateCalculator::UnpackDate(packed);
SCOPED_TRACE("Date: " + std::to_string(tc.year) + "-" +
std::to_string(tc.month) + "-" + std::to_string(tc.day));
ExpectDate(unpacked, tc.year, tc.month, tc.day);
}
}
TEST_F(HolidayDateCalculatorTest, PackUnpack_Roundtrip_FullRange)
{
// Test pack/unpack for entire valid range (2000-2031)
for (int year = 2000; year <= 2031; ++year)
{
for (int month = 1; month <= 12; ++month)
{
for (int day = 1; day <= 28; ++day) // Safe range for all months
{
std::tm original = {};
original.tm_year = year - 1900;
original.tm_mon = month - 1;
original.tm_mday = day;
mktime(&original);
uint32_t packed = HolidayDateCalculator::PackDate(original);
std::tm unpacked = HolidayDateCalculator::UnpackDate(packed);
EXPECT_EQ(original.tm_year, unpacked.tm_year);
EXPECT_EQ(original.tm_mon, unpacked.tm_mon);
EXPECT_EQ(original.tm_mday, unpacked.tm_mday);
}
}
}
}
TEST_F(HolidayDateCalculatorTest, UnpackDate_KnownValues)
{
struct UnpackTestCase { uint32_t packed; int year; int month; int day; };
std::vector<UnpackTestCase> testCases = {
{ 335921152, 2020, 1, 24 },
{ 352681984, 2021, 1, 23 },
{ 336707584, 2020, 2, 8 },
{ 346390592, 2020, 11, 23 },
};
for (auto const& tc : testCases)
{
std::tm date = HolidayDateCalculator::UnpackDate(tc.packed);
SCOPED_TRACE("Packed: " + std::to_string(tc.packed));
ExpectDate(date, tc.year, tc.month, tc.day);
}
}
// ============================================================
// Noblegarden (Easter-based) Tests - Extended Range
// ============================================================
TEST_F(HolidayDateCalculatorTest, Noblegarden_DayAfterEaster_1900_2200)
{
// Noblegarden should be Easter + 1 day (Monday after Easter) for all years
for (int year = 1900; year <= 2200; ++year)
{
std::tm easter = HolidayDateCalculator::CalculateEasterSunday(year);
// Calculate expected Noblegarden date (Easter + 1)
std::tm expectedNoblegarden = easter;
expectedNoblegarden.tm_mday += 1;
mktime(&expectedNoblegarden); // Normalize (handles month rollover)
// Get calculated Noblegarden from holiday rule
HolidayRule noblegarden = { 181, HolidayCalculationType::EASTER_OFFSET, 0, 0, 0, 1 };
std::tm calculated = HolidayDateCalculator::CalculateHolidayDate(noblegarden, year);
SCOPED_TRACE("Year: " + std::to_string(year));
EXPECT_EQ(calculated.tm_year, expectedNoblegarden.tm_year);
EXPECT_EQ(calculated.tm_mon, expectedNoblegarden.tm_mon);
EXPECT_EQ(calculated.tm_mday, expectedNoblegarden.tm_mday);
// Noblegarden should be Monday (1 day after Easter Sunday)
EXPECT_EQ(calculated.tm_wday, 1) << "Noblegarden should be Monday";
}
}
// ============================================================
// Pilgrim's Bounty (Thanksgiving) Tests - Extended Range
// ============================================================
TEST_F(HolidayDateCalculatorTest, PilgrimsBounty_SundayBeforeThanksgiving_1900_2200)
{
// Pilgrim's Bounty = Sunday before Thanksgiving (4th Thursday - 4 days)
for (int year = 1900; year <= 2200; ++year)
{
// Calculate 4th Thursday of November
std::tm thanksgiving = HolidayDateCalculator::CalculateNthWeekday(year, 11, Weekday::THURSDAY, 4);
// Pilgrim's Bounty starts on Sunday before (4 days earlier)
std::tm expectedPilgrims = thanksgiving;
expectedPilgrims.tm_mday -= 4;
mktime(&expectedPilgrims);
// Get calculated date using rule with -4 offset
HolidayRule pilgrimsBounty = { 404, HolidayCalculationType::NTH_WEEKDAY, 11, 4, static_cast<int>(Weekday::THURSDAY), -4 };
std::tm date = HolidayDateCalculator::CalculateHolidayDate(pilgrimsBounty, year);
SCOPED_TRACE("Year: " + std::to_string(year));
EXPECT_EQ(date.tm_year + 1900, year);
EXPECT_EQ(date.tm_mon + 1, 11); // November
EXPECT_EQ(date.tm_wday, 0); // Sunday
EXPECT_EQ(date.tm_mday, expectedPilgrims.tm_mday);
// Sunday before 4th Thursday should be between 18th and 24th
EXPECT_GE(date.tm_mday, 18);
EXPECT_LE(date.tm_mday, 24);
}
}
// ============================================================
// Fixed Date Holidays Tests - Extended Range
// ============================================================
TEST_F(HolidayDateCalculatorTest, FixedDateHolidays_ConsistentAcrossYears_1900_2200)
{
// Fixed date holidays should have same month/day every year
// Note: Brewfest, Harvest Festival, and Winter Veil are now dynamic (not fixed date)
struct FixedHolidayTestCase { uint32_t holidayId; int month; int day; const char* name; };
std::vector<FixedHolidayTestCase> testCases = {
{ 341, 6, 21, "Midsummer Fire Festival" },
{ 62, 7, 4, "Fireworks Spectacular" },
{ 398, 9, 19, "Pirates' Day" },
{ 324, 10, 18, "Hallow's End" },
{ 409, 11, 1, "Day of the Dead" },
};
for (auto const& tc : testCases)
{
HolidayRule rule = { tc.holidayId, HolidayCalculationType::FIXED_DATE, tc.month, tc.day, 0, 0 };
for (int year = 1900; year <= 2200; ++year)
{
std::tm date = HolidayDateCalculator::CalculateHolidayDate(rule, year);
SCOPED_TRACE(std::string(tc.name) + " Year: " + std::to_string(year));
EXPECT_EQ(date.tm_year + 1900, year);
EXPECT_EQ(date.tm_mon + 1, tc.month);
EXPECT_EQ(date.tm_mday, tc.day);
}
}
}
// ============================================================
// Brewfest Tests (Fixed Sept 20 main event)
// ============================================================
TEST_F(HolidayDateCalculatorTest, Brewfest_FixedSept13)
{
// Brewfest is now fixed: prep starts Sept 13, main event Sept 20
// This avoids any potential overlap with Pirates' Day (Sept 19)
HolidayRule brewfest = { 372, HolidayCalculationType::FIXED_DATE, 9, 13, 0, 0 };
for (int year = 2000; year <= 2030; ++year)
{
std::tm date = HolidayDateCalculator::CalculateHolidayDate(brewfest, year);
SCOPED_TRACE("Year: " + std::to_string(year));
EXPECT_EQ(date.tm_year + 1900, year);
EXPECT_EQ(date.tm_mon + 1, 9); // September
EXPECT_EQ(date.tm_mday, 13); // Always Sept 13
}
}
TEST_F(HolidayDateCalculatorTest, Brewfest_NoPiratesDayConflict)
{
// Brewfest main event (Sept 20) is always after Pirates' Day (Sept 19)
HolidayRule brewfest = { 372, HolidayCalculationType::FIXED_DATE, 9, 13, 0, 0 };
HolidayRule piratesDay = { 398, HolidayCalculationType::FIXED_DATE, 9, 19, 0, 0 };
for (int year = 2000; year <= 2030; ++year)
{
std::tm brewfestDate = HolidayDateCalculator::CalculateHolidayDate(brewfest, year);
std::tm piratesDate = HolidayDateCalculator::CalculateHolidayDate(piratesDay, year);
SCOPED_TRACE("Year: " + std::to_string(year));
// Brewfest prep is Sept 13, main event is Sept 20
// Pirates' Day is Sept 19, which falls between prep and main event
EXPECT_EQ(brewfestDate.tm_mday, 13); // Brewfest prep
EXPECT_EQ(piratesDate.tm_mday, 19); // Pirates' Day
// Main event (Sept 20) > Pirates' Day (Sept 19)
}
}
// ============================================================
// Harvest Festival Tests (Autumn Equinox based)
// 2 days before autumn equinox
// ============================================================
TEST_F(HolidayDateCalculatorTest, HarvestFestival_AutumnEquinoxBased)
{
HolidayRule harvestFestival = { 321, HolidayCalculationType::AUTUMN_EQUINOX, 0, 0, 0, -2 };
// Autumn equinox is typically Sept 22-23, so Harvest Festival is Sept 20-21
for (int year = 2000; year <= 2030; ++year)
{
std::tm equinox = HolidayDateCalculator::CalculateAutumnEquinox(year);
std::tm harvest = HolidayDateCalculator::CalculateHolidayDate(harvestFestival, year);
SCOPED_TRACE("Year: " + std::to_string(year));
// Harvest should be exactly 2 days before equinox
// Convert to time_t to handle month boundaries correctly
time_t equinoxTime = mktime(&equinox);
time_t harvestTime = mktime(&harvest);
double diffDays = difftime(equinoxTime, harvestTime) / (60 * 60 * 24);
EXPECT_NEAR(diffDays, 2.0, 0.1) << "Harvest Festival should be 2 days before equinox";
}
}
TEST_F(HolidayDateCalculatorTest, HarvestFestival_AlwaysInSeptember_1900_2200)
{
HolidayRule harvestFestival = { 321, HolidayCalculationType::AUTUMN_EQUINOX, 0, 0, 0, -2 };
for (int year = 1900; year <= 2200; ++year)
{
std::tm date = HolidayDateCalculator::CalculateHolidayDate(harvestFestival, year);
SCOPED_TRACE("Year: " + std::to_string(year));
// Harvest Festival should always be in September (2 days before Sept 22-23 equinox)
EXPECT_EQ(date.tm_mon + 1, 9) << "Harvest Festival should be in September";
// Should be around Sept 20-21
EXPECT_GE(date.tm_mday, 18) << "Harvest Festival should be >= Sept 18";
EXPECT_LE(date.tm_mday, 22) << "Harvest Festival should be <= Sept 22";
}
}
// ============================================================
// Winter Veil Tests (Winter Solstice based)
// 6 days before winter solstice
// ============================================================
TEST_F(HolidayDateCalculatorTest, WinterVeil_WinterSolsticeBased)
{
HolidayRule winterVeil = { 141, HolidayCalculationType::WINTER_SOLSTICE, 0, 0, 0, -6 };
// Winter solstice is typically Dec 21-22, so Winter Veil starts Dec 15-16
for (int year = 2000; year <= 2030; ++year)
{
std::tm solstice = HolidayDateCalculator::CalculateWinterSolstice(year);
std::tm winterVeilDate = HolidayDateCalculator::CalculateHolidayDate(winterVeil, year);
SCOPED_TRACE("Year: " + std::to_string(year));
// Winter Veil should be exactly 6 days before solstice
time_t solsticeTime = mktime(&solstice);
time_t winterVeilTime = mktime(&winterVeilDate);
double diffDays = difftime(solsticeTime, winterVeilTime) / (60 * 60 * 24);
EXPECT_NEAR(diffDays, 6.0, 0.1) << "Winter Veil should be 6 days before solstice";
}
}
TEST_F(HolidayDateCalculatorTest, WinterVeil_AlwaysInDecember_1900_2200)
{
HolidayRule winterVeil = { 141, HolidayCalculationType::WINTER_SOLSTICE, 0, 0, 0, -6 };
for (int year = 1900; year <= 2200; ++year)
{
std::tm date = HolidayDateCalculator::CalculateHolidayDate(winterVeil, year);
SCOPED_TRACE("Year: " + std::to_string(year));
// Winter Veil should always be in December
EXPECT_EQ(date.tm_mon + 1, 12) << "Winter Veil should be in December";
// Should be around Dec 15-16 (6 days before Dec 21-22)
EXPECT_GE(date.tm_mday, 14) << "Winter Veil should be >= Dec 14";
EXPECT_LE(date.tm_mday, 17) << "Winter Veil should be <= Dec 17";
}
}
// ============================================================
// Love is in the Air (First Monday on or after Feb 3)
// ============================================================
TEST_F(HolidayDateCalculatorTest, LoveIsInTheAir_FirstMondayOnOrAfterFeb3)
{
// Verify "first Monday on or after Feb 3" calculation
struct LoveTestCase { int year; int expectedDay; };
std::vector<LoveTestCase> testCases = {
{ 2024, 5 }, // Feb 3 is Sat, first Mon after is Feb 5
{ 2025, 3 }, // Feb 3 is Mon, so Feb 3
{ 2026, 9 }, // Feb 3 is Tue, first Mon after is Feb 9
{ 2027, 8 }, // Feb 3 is Wed, first Mon after is Feb 8
{ 2028, 7 }, // Feb 3 is Thu, first Mon after is Feb 7
{ 2029, 5 }, // Feb 3 is Sat, first Mon after is Feb 5
{ 2030, 4 }, // Feb 3 is Sun, first Mon after is Feb 4
};
for (auto const& tc : testCases)
{
std::tm date = HolidayDateCalculator::CalculateWeekdayOnOrAfter(tc.year, 2, 3, Weekday::MONDAY);
SCOPED_TRACE("Year: " + std::to_string(tc.year));
EXPECT_EQ(date.tm_year + 1900, tc.year);
EXPECT_EQ(date.tm_mon + 1, 2); // February
EXPECT_EQ(date.tm_mday, tc.expectedDay);
EXPECT_EQ(date.tm_wday, 1); // Monday
}
}
TEST_F(HolidayDateCalculatorTest, ChildrensWeek_FirstMondayOnOrAfterApr25)
{
// Verify "first Monday on or after Apr 25" calculation (Monday closest to May 1)
struct ChildrensWeekTestCase { int year; int expectedMonth; int expectedDay; };
std::vector<ChildrensWeekTestCase> testCases = {
{ 2023, 5, 1 }, // Apr 25 is Tue, first Mon after is May 1
{ 2024, 4, 29 }, // Apr 25 is Thu, first Mon after is Apr 29
{ 2025, 4, 28 }, // Apr 25 is Fri, first Mon after is Apr 28
{ 2026, 4, 27 }, // Apr 25 is Sat, first Mon after is Apr 27
{ 2027, 4, 26 }, // Apr 25 is Sun, first Mon after is Apr 26
};
for (auto const& tc : testCases)
{
std::tm date = HolidayDateCalculator::CalculateWeekdayOnOrAfter(tc.year, 4, 25, Weekday::MONDAY);
SCOPED_TRACE("Year: " + std::to_string(tc.year));
EXPECT_EQ(date.tm_year + 1900, tc.year);
EXPECT_EQ(date.tm_mon + 1, tc.expectedMonth);
EXPECT_EQ(date.tm_mday, tc.expectedDay);
EXPECT_EQ(date.tm_wday, 1); // Monday
}
}
TEST_F(HolidayDateCalculatorTest, WeekdayOnOrAfter_AlwaysCorrectWeekday_1900_2200)
{
// Verify the result is always the correct weekday for entire range
for (int year = 1900; year <= 2200; ++year)
{
for (int weekday = 0; weekday <= 6; ++weekday)
{
std::tm date = HolidayDateCalculator::CalculateWeekdayOnOrAfter(year, 2, 3, static_cast<Weekday>(weekday));
SCOPED_TRACE("Year: " + std::to_string(year) + " Weekday: " + std::to_string(weekday));
EXPECT_EQ(date.tm_wday, weekday);
EXPECT_EQ(date.tm_mon + 1, 2); // Should stay in February
EXPECT_GE(date.tm_mday, 3); // Should be on or after Feb 3
EXPECT_LE(date.tm_mday, 9); // At most 6 days later
}
}
}
TEST_F(HolidayDateCalculatorTest, WeekdayOnOrAfter_MonthBoundary_RollsIntoNextMonth)
{
// Test dates near month-end that may roll into the next month
// Apr 25 looking for Monday can roll into May (e.g., if Apr 25 is Sunday, Monday is May 1)
for (int year = 1900; year <= 2200; ++year)
{
// Test Apr 25 (Children's Week reference date)
std::tm apr25 = HolidayDateCalculator::CalculateWeekdayOnOrAfter(year, 4, 25, Weekday::MONDAY);
SCOPED_TRACE("Apr 25, Year: " + std::to_string(year));
EXPECT_EQ(apr25.tm_wday, 1); // Monday
EXPECT_TRUE(apr25.tm_mon == 3 || apr25.tm_mon == 4); // April or May (0-indexed: 3 or 4)
// If still in April, must be >= 25. If in May, can be 1-6.
if (apr25.tm_mon == 3)
EXPECT_GE(apr25.tm_mday, 25);
else
EXPECT_LE(apr25.tm_mday, 6);
// Test Apr 30 - more likely to roll into May
std::tm apr30 = HolidayDateCalculator::CalculateWeekdayOnOrAfter(year, 4, 30, Weekday::MONDAY);
SCOPED_TRACE("Apr 30, Year: " + std::to_string(year));
EXPECT_EQ(apr30.tm_wday, 1); // Monday
EXPECT_TRUE(apr30.tm_mon == 3 || apr30.tm_mon == 4); // April or May
if (apr30.tm_mon == 3)
EXPECT_EQ(apr30.tm_mday, 30); // Apr 30 must be the Monday
else
EXPECT_LE(apr30.tm_mday, 6); // May 1-6
// Test Dec 31 - can roll into January of next year
std::tm dec31 = HolidayDateCalculator::CalculateWeekdayOnOrAfter(year, 12, 31, Weekday::MONDAY);
SCOPED_TRACE("Dec 31, Year: " + std::to_string(year));
EXPECT_EQ(dec31.tm_wday, 1); // Monday
// Could be Dec 31 or Jan 1-6 of next year
EXPECT_TRUE((dec31.tm_mon == 11 && dec31.tm_mday == 31) ||
(dec31.tm_mon == 0 && dec31.tm_mday <= 6));
}
}
// ============================================================
// Stress Tests - Verify No Crashes or Invalid Dates
// ============================================================
TEST_F(HolidayDateCalculatorTest, StressTest_AllCalculations_1900_2200)
{
// Run all holiday calculations for entire range to ensure no crashes
const std::vector<HolidayRule>& rules = HolidayDateCalculator::GetHolidayRules();
int totalCalculations = 0;
for (int year = 1900; year <= 2200; ++year)
{
// Test Easter
std::tm easter = HolidayDateCalculator::CalculateEasterSunday(year);
EXPECT_TRUE(IsValidDate(year, easter.tm_mon + 1, easter.tm_mday))
<< "Invalid Easter date for year " << year;
totalCalculations++;
// Test all weekday calculations
for (int month = 1; month <= 12; ++month)
{
for (int n = 1; n <= 4; ++n)
{
std::tm date = HolidayDateCalculator::CalculateNthWeekday(year, month, Weekday::THURSDAY, n);
EXPECT_TRUE(IsValidDate(year, date.tm_mon + 1, date.tm_mday))
<< "Invalid NthWeekday date for year " << year << " month " << month << " n=" << n;
totalCalculations++;
}
}
// Test all holiday rules
for (auto const& rule : rules)
{
std::tm date = HolidayDateCalculator::CalculateHolidayDate(rule, year);
EXPECT_TRUE(IsValidDate(year, date.tm_mon + 1, date.tm_mday))
<< "Invalid holiday date for holiday " << rule.holidayId << " year " << year;
totalCalculations++;
}
}
// Verify we ran a significant number of calculations
// 301 years * (1 Easter + 48 NthWeekday + 6 holidays) = 301 * 55 = 16555
EXPECT_GT(totalCalculations, 15000) << "Should have run many calculations";
}
// ============================================================
// Edge Case Tests
// ============================================================
TEST_F(HolidayDateCalculatorTest, LeapYear_AllYears_1900_2200)
{
// Verify calculations work correctly for all years, checking leap year logic
for (int year = 1900; year <= 2200; ++year)
{
bool expectedLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
SCOPED_TRACE("Year: " + std::to_string(year));
EXPECT_EQ(IsLeapYear(year), expectedLeap);
// Easter calculation should always work regardless of leap year
std::tm easter = HolidayDateCalculator::CalculateEasterSunday(year);
EXPECT_EQ(easter.tm_wday, 0) << "Easter should be Sunday";
// All holiday calculations should produce valid dates
for (auto const& rule : HolidayDateCalculator::GetHolidayRules())
{
std::tm date = HolidayDateCalculator::CalculateHolidayDate(rule, year);
EXPECT_TRUE(IsValidDate(year, date.tm_mon + 1, date.tm_mday))
<< "Invalid date for holiday " << rule.holidayId;
}
}
}
TEST_F(HolidayDateCalculatorTest, GetPackedHolidayDate_UnknownHoliday)
{
// Unknown holiday ID should return 0
uint32_t result = HolidayDateCalculator::GetPackedHolidayDate(99999, 2025);
EXPECT_EQ(result, 0u);
}
TEST_F(HolidayDateCalculatorTest, PackDate_YearBeyond2031)
{
// WoW's packed date format uses 5 bits for year offset from 2000
// - Offsets 0-30 represent specific years 2000-2030
// - Offset 31 is a special marker meaning "repeats every year" (used for fixed-date holidays)
// This is a Blizzard client limitation, not an emulator design choice
//
// Years beyond 2031 will overflow when unpacked due to the 5-bit mask
// Test year 2032 (offset 32) - demonstrates the overflow behavior
{
std::tm date = {};
date.tm_year = 2032 - 1900;
date.tm_mon = 5; // June
date.tm_mday = 15;
mktime(&date);
uint32_t packed = HolidayDateCalculator::PackDate(date);
std::tm unpacked = HolidayDateCalculator::UnpackDate(packed);
// Year offset 32 masked with 0x1F (5 bits) = 0, so unpacked year = 2000
// This documents the WoW client's inherent year 2031 limitation
EXPECT_EQ(unpacked.tm_year + 1900, 2000) << "Year 2032 wraps to 2000 due to 5-bit WoW client format";
EXPECT_EQ(unpacked.tm_mon + 1, 6);
EXPECT_EQ(unpacked.tm_mday, 15);
}
// Test year 2035 (offset 35)
{
std::tm date = {};
date.tm_year = 2035 - 1900;
date.tm_mon = 0; // January
date.tm_mday = 1;
mktime(&date);
uint32_t packed = HolidayDateCalculator::PackDate(date);
std::tm unpacked = HolidayDateCalculator::UnpackDate(packed);
// Year offset 35 masked with 0x1F = 3, so unpacked year = 2003
EXPECT_EQ(unpacked.tm_year + 1900, 2003) << "Year 2035 wraps to 2003 due to 5-bit WoW client format";
}
// Test boundary: year 2030 is the last fully usable year (offset 30)
// (offset 31 is reserved for "repeating yearly" holidays)
{
std::tm date = {};
date.tm_year = 2030 - 1900;
date.tm_mon = 11; // December
date.tm_mday = 31;
mktime(&date);
uint32_t packed = HolidayDateCalculator::PackDate(date);
std::tm unpacked = HolidayDateCalculator::UnpackDate(packed);
EXPECT_EQ(unpacked.tm_year + 1900, 2030) << "Year 2030 should pack/unpack correctly";
EXPECT_EQ(unpacked.tm_mon + 1, 12);
EXPECT_EQ(unpacked.tm_mday, 31);
}
}
TEST_F(HolidayDateCalculatorTest, CenturyBoundaries)
{
// Test calculations around century boundaries (which affect leap year rules)
std::vector<int> centuryYears = { 1900, 2000, 2100, 2200 };
for (int year : centuryYears)
{
SCOPED_TRACE("Century year: " + std::to_string(year));
// Easter
std::tm easter = HolidayDateCalculator::CalculateEasterSunday(year);
EXPECT_EQ(easter.tm_wday, 0) << "Easter should be Sunday";
EXPECT_TRUE(easter.tm_mon == 2 || easter.tm_mon == 3) << "Easter should be in March or April";
// Thanksgiving
std::tm thanksgiving = HolidayDateCalculator::CalculateNthWeekday(year, 11, Weekday::THURSDAY, 4);
EXPECT_EQ(thanksgiving.tm_wday, static_cast<int>(Weekday::THURSDAY));
EXPECT_EQ(thanksgiving.tm_mon + 1, 11);
}
}
// ============================================================
// Lunar New Year (Chinese New Year) Tests - Extended Range
// Based on Jean Meeus "Astronomical Algorithms" (1991)
// ============================================================
TEST_F(HolidayDateCalculatorTest, LunarNewYear_KnownDates)
{
// Verify against known Chinese New Year dates
// Source: Official astronomical calculations and historical records
struct LunarNewYearTestCase { int year; int month; int day; };
std::vector<LunarNewYearTestCase> testCases = {
// Historical dates (2000-2010)
{ 2000, 2, 5 },
{ 2001, 1, 24 },
{ 2002, 2, 12 },
{ 2003, 2, 1 },
{ 2004, 1, 22 },
{ 2005, 2, 9 },
{ 2006, 1, 29 },
{ 2007, 2, 18 },
{ 2008, 2, 7 },
{ 2009, 1, 26 },
{ 2010, 2, 14 },
// Recent dates (2011-2020)
{ 2011, 2, 3 },
{ 2012, 1, 23 },
{ 2013, 2, 10 },
{ 2014, 1, 31 },
{ 2015, 2, 19 },
{ 2016, 2, 8 },
{ 2017, 1, 28 },
{ 2018, 2, 16 },
{ 2019, 2, 5 },
{ 2020, 1, 25 },
// Current and near-future dates (2021-2031)
{ 2021, 2, 12 },
{ 2022, 2, 1 },
{ 2023, 1, 22 },
{ 2024, 2, 10 },
{ 2025, 1, 29 },
{ 2026, 2, 17 },
{ 2027, 2, 6 },
{ 2028, 1, 26 },
{ 2029, 2, 13 },
{ 2030, 2, 3 },
{ 2031, 1, 23 },
};
for (auto const& tc : testCases)
{
std::tm lunarNewYear = HolidayDateCalculator::CalculateLunarNewYear(tc.year);
SCOPED_TRACE("Year: " + std::to_string(tc.year));
ExpectDate(lunarNewYear, tc.year, tc.month, tc.day);
}
}
TEST_F(HolidayDateCalculatorTest, LunarNewYear_ValidDateRange_1900_2200)
{
// Chinese New Year must always fall between January 21 and February 20 (inclusive)
// This is a fundamental property of the lunisolar calendar
for (int year = 1900; year <= 2200; ++year)
{
std::tm lunarNewYear = HolidayDateCalculator::CalculateLunarNewYear(year);
SCOPED_TRACE("Year: " + std::to_string(year));
// Verify year is correct
EXPECT_EQ(lunarNewYear.tm_year + 1900, year);
// Chinese New Year must be in January or February
EXPECT_TRUE(lunarNewYear.tm_mon == 0 || lunarNewYear.tm_mon == 1)
<< "Lunar New Year must be in January (0) or February (1), got month " << lunarNewYear.tm_mon;
// Valid range: January 21 - February 20
if (lunarNewYear.tm_mon == 0) // January
{
EXPECT_GE(lunarNewYear.tm_mday, 21) << "Lunar New Year in January must be >= 21";
EXPECT_LE(lunarNewYear.tm_mday, 31) << "Lunar New Year in January must be <= 31";
}
else // February
{
EXPECT_GE(lunarNewYear.tm_mday, 1) << "Lunar New Year in February must be >= 1";
EXPECT_LE(lunarNewYear.tm_mday, 20) << "Lunar New Year in February must be <= 20";
}
// Verify it's a valid calendar date
EXPECT_TRUE(IsValidDate(year, lunarNewYear.tm_mon + 1, lunarNewYear.tm_mday))
<< "Lunar New Year should be a valid calendar date";
}
}
TEST_F(HolidayDateCalculatorTest, LunarFestival_DayBeforeChineseNewYear)
{
// Lunar Festival starts 1 day before Chinese New Year
// Test with -1 offset applied
HolidayRule lunarFestival = { 327, HolidayCalculationType::LUNAR_NEW_YEAR, 0, 0, 0, -1 };
for (int year = 2000; year <= 2100; ++year)
{
std::tm fromRule = HolidayDateCalculator::CalculateHolidayDate(lunarFestival, year);
std::tm cny = HolidayDateCalculator::CalculateLunarNewYear(year);
// Expected: CNY - 1 day
std::tm expected = cny;
expected.tm_mday -= 1;
mktime(&expected);
SCOPED_TRACE("Year: " + std::to_string(year));
// Lunar Festival should be 1 day before Chinese New Year
EXPECT_EQ(fromRule.tm_year, expected.tm_year);
EXPECT_EQ(fromRule.tm_mon, expected.tm_mon);
EXPECT_EQ(fromRule.tm_mday, expected.tm_mday);
}
}
TEST_F(HolidayDateCalculatorTest, LunarFestival_KnownDates)
{
// Verify Lunar Festival (CNY - 1 day) against known official dates
// Source: WoW official event announcements
struct LunarFestivalTestCase { int year; int month; int day; };
std::vector<LunarFestivalTestCase> testCases = {
{ 2024, 2, 9 }, // CNY Feb 10 - 1 = Feb 9
{ 2025, 1, 28 }, // CNY Jan 29 - 1 = Jan 28 (confirmed official)
{ 2026, 2, 16 }, // CNY Feb 17 - 1 = Feb 16
{ 2027, 2, 5 }, // CNY Feb 6 - 1 = Feb 5
};
HolidayRule lunarFestival = { 327, HolidayCalculationType::LUNAR_NEW_YEAR, 0, 0, 0, -1 };
for (auto const& tc : testCases)
{
std::tm date = HolidayDateCalculator::CalculateHolidayDate(lunarFestival, tc.year);
SCOPED_TRACE("Year: " + std::to_string(tc.year));
ExpectDate(date, tc.year, tc.month, tc.day);
}
}
TEST_F(HolidayDateCalculatorTest, LunarNewYear_NoRepeatedDates)
{
// Each year should have a unique Chinese New Year date
// (no two consecutive years should have the exact same month/day)
int prevMonth = -1;
int prevDay = -1;
for (int year = 1900; year <= 2200; ++year)
{
std::tm lunarNewYear = HolidayDateCalculator::CalculateLunarNewYear(year);
if (prevMonth != -1)
{
// The date should be different from previous year
// (due to ~11 day lunar cycle drift)
bool sameDate = (lunarNewYear.tm_mon == prevMonth && lunarNewYear.tm_mday == prevDay);
EXPECT_FALSE(sameDate)
<< "Year " << year << " has same date as previous year: "
<< (lunarNewYear.tm_mon + 1) << "/" << lunarNewYear.tm_mday;
}
prevMonth = lunarNewYear.tm_mon;
prevDay = lunarNewYear.tm_mday;
}
}
TEST_F(HolidayDateCalculatorTest, LunarNewYear_19YearMetonicCycle)
{
// The Chinese calendar roughly follows a 19-year Metonic cycle
// Dates should approximately repeat every 19 years (within a few days)
for (int year = 1900; year <= 2180; ++year)
{
std::tm date1 = HolidayDateCalculator::CalculateLunarNewYear(year);
std::tm date2 = HolidayDateCalculator::CalculateLunarNewYear(year + 19);
SCOPED_TRACE("Comparing year " + std::to_string(year) + " with " + std::to_string(year + 19));
// Convert to day-of-year for easier comparison
int doy1 = (date1.tm_mon == 0) ? date1.tm_mday : 31 + date1.tm_mday;
int doy2 = (date2.tm_mon == 0) ? date2.tm_mday : 31 + date2.tm_mday;
// The Metonic cycle is approximate - typically within a few days, but can shift
// by up to a lunar month (~29 days) at cycle boundaries due to intercalary months
int diff = std::abs(doy1 - doy2);
EXPECT_LE(diff, 30) << "19-year Metonic cycle should keep dates within one lunar month";
}
}
// ============================================================
// Darkmoon Faire Tests
// First Sunday of the month, rotating between 3 locations
// ============================================================
TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_LocationRotation)
{
// Verify the 3-location rotation pattern:
// Mulgore (offset 1): Jan, Apr, Jul, Oct - month % 3 == 1
// Terokkar (offset 2): Feb, May, Aug, Nov - month % 3 == 2
// Elwynn (offset 0): Mar, Jun, Sep, Dec - month % 3 == 0
// Mulgore months (offset 1)
EXPECT_EQ(1 % 3, 1);
EXPECT_EQ(4 % 3, 1);
EXPECT_EQ(7 % 3, 1);
EXPECT_EQ(10 % 3, 1);
// Terokkar months (offset 2)
EXPECT_EQ(2 % 3, 2);
EXPECT_EQ(5 % 3, 2);
EXPECT_EQ(8 % 3, 2);
EXPECT_EQ(11 % 3, 2);
// Elwynn months (offset 0)
EXPECT_EQ(3 % 3, 0);
EXPECT_EQ(6 % 3, 0);
EXPECT_EQ(9 % 3, 0);
EXPECT_EQ(12 % 3, 0);
}
TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_FirstSundayOfMonth_KnownDates)
{
// Verify first Sunday calculation against known dates
struct FirstSundayTestCase { int year; int month; int expectedDay; };
std::vector<FirstSundayTestCase> testCases = {
// 2024
{ 2024, 1, 7 }, // Jan 2024: First Sunday = Jan 7
{ 2024, 2, 4 }, // Feb 2024: First Sunday = Feb 4
{ 2024, 3, 3 }, // Mar 2024: First Sunday = Mar 3
{ 2024, 4, 7 }, // Apr 2024: First Sunday = Apr 7
{ 2024, 9, 1 }, // Sep 2024: First Sunday = Sep 1
{ 2024, 12, 1 }, // Dec 2024: First Sunday = Dec 1
// 2025
{ 2025, 1, 5 }, // Jan 2025: First Sunday = Jan 5
{ 2025, 2, 2 }, // Feb 2025: First Sunday = Feb 2
{ 2025, 3, 2 }, // Mar 2025: First Sunday = Mar 2
{ 2025, 6, 1 }, // Jun 2025: First Sunday = Jun 1
{ 2025, 9, 7 }, // Sep 2025: First Sunday = Sep 7
{ 2025, 12, 7 }, // Dec 2025: First Sunday = Dec 7
// 2026
{ 2026, 1, 4 }, // Jan 2026: First Sunday = Jan 4
{ 2026, 3, 1 }, // Mar 2026: First Sunday = Mar 1
};
for (auto const& tc : testCases)
{
std::tm date = HolidayDateCalculator::CalculateNthWeekday(tc.year, tc.month, Weekday::SUNDAY, 1);
SCOPED_TRACE("Year: " + std::to_string(tc.year) + " Month: " + std::to_string(tc.month));
EXPECT_EQ(date.tm_year + 1900, tc.year);
EXPECT_EQ(date.tm_mon + 1, tc.month);
EXPECT_EQ(date.tm_mday, tc.expectedDay);
EXPECT_EQ(date.tm_wday, 0) << "Should be Sunday";
}
}
TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_GetDates_Elwynn)
{
// Elwynn (offset 0): Mar, Jun, Sep, Dec
std::vector<uint32_t> dates = HolidayDateCalculator::GetDarkmoonFaireDates(0, 2025, 1);
// Should have exactly 4 dates for one year
EXPECT_EQ(dates.size(), 4u);
// Verify each date is in the correct month and is a Sunday
std::vector<int> expectedMonths = { 3, 6, 9, 12 };
for (size_t i = 0; i < dates.size(); ++i)
{
std::tm date = HolidayDateCalculator::UnpackDate(dates[i]);
SCOPED_TRACE("Date index: " + std::to_string(i));
EXPECT_EQ(date.tm_year + 1900, 2025);
EXPECT_EQ(date.tm_mon + 1, expectedMonths[i]);
EXPECT_EQ(date.tm_wday, 0) << "Should be Sunday";
EXPECT_GE(date.tm_mday, 1);
EXPECT_LE(date.tm_mday, 7) << "First Sunday must be within first 7 days";
}
}
TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_GetDates_Mulgore)
{
// Mulgore (offset 1): Jan, Apr, Jul, Oct
std::vector<uint32_t> dates = HolidayDateCalculator::GetDarkmoonFaireDates(1, 2025, 1);
// Should have exactly 4 dates for one year
EXPECT_EQ(dates.size(), 4u);
std::vector<int> expectedMonths = { 1, 4, 7, 10 };
for (size_t i = 0; i < dates.size(); ++i)
{
std::tm date = HolidayDateCalculator::UnpackDate(dates[i]);
SCOPED_TRACE("Date index: " + std::to_string(i));
EXPECT_EQ(date.tm_year + 1900, 2025);
EXPECT_EQ(date.tm_mon + 1, expectedMonths[i]);
EXPECT_EQ(date.tm_wday, 0) << "Should be Sunday";
}
}
TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_GetDates_Terokkar)
{
// Terokkar (offset 2): Feb, May, Aug, Nov
std::vector<uint32_t> dates = HolidayDateCalculator::GetDarkmoonFaireDates(2, 2025, 1);
// Should have exactly 4 dates for one year
EXPECT_EQ(dates.size(), 4u);
std::vector<int> expectedMonths = { 2, 5, 8, 11 };
for (size_t i = 0; i < dates.size(); ++i)
{
std::tm date = HolidayDateCalculator::UnpackDate(dates[i]);
SCOPED_TRACE("Date index: " + std::to_string(i));
EXPECT_EQ(date.tm_year + 1900, 2025);
EXPECT_EQ(date.tm_mon + 1, expectedMonths[i]);
EXPECT_EQ(date.tm_wday, 0) << "Should be Sunday";
}
}
TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_GetDates_MultiYear)
{
// Get 4 years of dates for Elwynn (offset 0)
std::vector<uint32_t> dates = HolidayDateCalculator::GetDarkmoonFaireDates(0, 2025, 4);
// 4 dates per year * 4 years = 16 dates
EXPECT_EQ(dates.size(), 16u);
// Verify dates are in chronological order
for (size_t i = 1; i < dates.size(); ++i)
{
std::tm prev = HolidayDateCalculator::UnpackDate(dates[i - 1]);
std::tm curr = HolidayDateCalculator::UnpackDate(dates[i]);
time_t prevTime = mktime(&prev);
time_t currTime = mktime(&curr);
EXPECT_GT(currTime, prevTime) << "Dates should be in chronological order";
}
}
TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_AlwaysSunday_AllLocations_2000_2030)
{
// Verify all Darkmoon Faire dates are Sundays for entire valid range
// 3 locations: offset 0 (Elwynn), offset 1 (Mulgore), offset 2 (Terokkar)
for (int offset = 0; offset <= 2; ++offset)
{
std::vector<uint32_t> dates = HolidayDateCalculator::GetDarkmoonFaireDates(offset, 2000, 31);
SCOPED_TRACE("Location offset: " + std::to_string(offset));
for (size_t i = 0; i < dates.size(); ++i)
{
std::tm date = HolidayDateCalculator::UnpackDate(dates[i]);
EXPECT_EQ(date.tm_wday, 0)
<< "Date " << (date.tm_year + 1900) << "-" << (date.tm_mon + 1) << "-" << date.tm_mday
<< " should be Sunday";
}
}
}
TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_CalculateHolidayDate_ReturnsFirstOccurrence)
{
// Using CalculateHolidayDate with DARKMOON_FAIRE should return the first occurrence of the year
// Elwynn (offset 0) = Mar/Jun/Sep/Dec, Mulgore (offset 1) = Jan/Apr/Jul/Oct, Terokkar (offset 2) = Feb/May/Aug/Nov
HolidayRule elwynnRule = { 374, HolidayCalculationType::DARKMOON_FAIRE, 0, 0, 0, -2 };
HolidayRule mulgoreRule = { 375, HolidayCalculationType::DARKMOON_FAIRE, 1, 0, 0, -2 };
HolidayRule terokkarRule = { 376, HolidayCalculationType::DARKMOON_FAIRE, 2, 0, 0, -2 };
// 2025 first occurrences:
// Elwynn (offset 0): March (first month where month % 3 == 0)
std::tm elwynn2025 = HolidayDateCalculator::CalculateHolidayDate(elwynnRule, 2025);
EXPECT_EQ(elwynn2025.tm_mon + 1, 3) << "Elwynn first occurrence should be March";
EXPECT_EQ(elwynn2025.tm_wday, 0) << "Should be Sunday";
// Mulgore (offset 1): January (first month where month % 3 == 1)
std::tm mulgore2025 = HolidayDateCalculator::CalculateHolidayDate(mulgoreRule, 2025);
EXPECT_EQ(mulgore2025.tm_mon + 1, 1) << "Mulgore first occurrence should be January";
EXPECT_EQ(mulgore2025.tm_wday, 0) << "Should be Sunday";
// Terokkar (offset 2): February (first month where month % 3 == 2)
std::tm terokkar2025 = HolidayDateCalculator::CalculateHolidayDate(terokkarRule, 2025);
EXPECT_EQ(terokkar2025.tm_mon + 1, 2) << "Terokkar first occurrence should be February";
EXPECT_EQ(terokkar2025.tm_wday, 0) << "Should be Sunday";
}
TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_NoOverlap_AllLocations)
{
// Verify all three locations don't share dates (they're in different months)
for (int year = 2000; year <= 2030; ++year)
{
std::vector<uint32_t> elwynn = HolidayDateCalculator::GetDarkmoonFaireDates(0, year, 1); // Mar/Jun/Sep/Dec
std::vector<uint32_t> mulgore = HolidayDateCalculator::GetDarkmoonFaireDates(1, year, 1); // Jan/Apr/Jul/Oct
std::vector<uint32_t> terokkar = HolidayDateCalculator::GetDarkmoonFaireDates(2, year, 1); // Feb/May/Aug/Nov
SCOPED_TRACE("Year: " + std::to_string(year));
// Check no overlap between any locations
for (auto const& e : elwynn)
{
for (auto const& m : mulgore)
EXPECT_NE(e, m) << "Elwynn and Mulgore should not share dates";
for (auto const& t : terokkar)
EXPECT_NE(e, t) << "Elwynn and Terokkar should not share dates";
}
for (auto const& m : mulgore)
{
for (auto const& t : terokkar)
EXPECT_NE(m, t) << "Mulgore and Terokkar should not share dates";
}
}
}
TEST_F(HolidayDateCalculatorTest, DarkmoonFaire_InHolidayRules)
{
// Verify all three Darkmoon Faire locations are in the HolidayRules
auto const& rules = HolidayDateCalculator::GetHolidayRules();
bool foundElwynn = false, foundMulgore = false, foundTerokkar = false;
for (auto const& rule : rules)
{
if (rule.holidayId == 374 && rule.type == HolidayCalculationType::DARKMOON_FAIRE)
foundElwynn = true;
if (rule.holidayId == 375 && rule.type == HolidayCalculationType::DARKMOON_FAIRE)
foundMulgore = true;
if (rule.holidayId == 376 && rule.type == HolidayCalculationType::DARKMOON_FAIRE)
foundTerokkar = true;
}
EXPECT_TRUE(foundElwynn) << "Darkmoon Faire Elwynn (374) should be in HolidayRules";
EXPECT_TRUE(foundMulgore) << "Darkmoon Faire Mulgore (375) should be in HolidayRules";
EXPECT_TRUE(foundTerokkar) << "Darkmoon Faire Terokkar (376) should be in HolidayRules";
}
// ============================================================================
// FindStartTimeForStage tests
// ============================================================================
class FindStartTimeForStageTest : public ::testing::Test
{
protected:
// Helper to create a time_t from year/month/day/hour
static time_t MakeTime(int year, int month, int day, int hour = 0)
{
std::tm t = {};
t.tm_year = year - 1900;
t.tm_mon = month - 1;
t.tm_mday = day;
t.tm_hour = hour;
t.tm_isdst = -1;
return mktime(&t);
}
// Pack two dates into an array (rest zeroed)
void PackTwoDates(uint32_t* dates, int y1, int m1, int d1, int y2, int m2, int d2)
{
std::tm t1 = {};
t1.tm_year = y1 - 1900;
t1.tm_mon = m1 - 1;
t1.tm_mday = d1;
t1.tm_isdst = -1;
mktime(&t1);
dates[0] = HolidayDateCalculator::PackDate(t1);
std::tm t2 = {};
t2.tm_year = y2 - 1900;
t2.tm_mon = m2 - 1;
t2.tm_mday = d2;
t2.tm_isdst = -1;
mktime(&t2);
dates[1] = HolidayDateCalculator::PackDate(t2);
for (int i = 2; i < 26; ++i)
dates[i] = 0;
}
};
// Stage 1 (no offset): curTime before event starts -> selects first date
TEST_F(FindStartTimeForStageTest, Stage1_BeforeStart_SelectsFirstDate)
{
uint32_t dates[26] = {};
PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3);
time_t curTime = MakeTime(2026, 3, 1);
time_t stageOffset = 0;
uint32_t stageLengthMin = 72 * 60; // 72 hours = 3 days
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
EXPECT_EQ(result, MakeTime(2026, 3, 6));
}
// Stage 1 (no offset): curTime during event -> selects current date
TEST_F(FindStartTimeForStageTest, Stage1_DuringEvent_SelectsCurrentDate)
{
uint32_t dates[26] = {};
PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3);
time_t curTime = MakeTime(2026, 3, 7, 12); // mid-event
time_t stageOffset = 0;
uint32_t stageLengthMin = 72 * 60; // 3 days
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
EXPECT_EQ(result, MakeTime(2026, 3, 6));
}
// Stage 1 (no offset): curTime after event ends -> selects next date
TEST_F(FindStartTimeForStageTest, Stage1_AfterEnd_SelectsNextDate)
{
uint32_t dates[26] = {};
PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3);
time_t curTime = MakeTime(2026, 3, 20); // well past first event
time_t stageOffset = 0;
uint32_t stageLengthMin = 72 * 60; // 3 days
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
EXPECT_EQ(result, MakeTime(2026, 4, 3));
}
// THE BUG: Stage 2 with stageOffset > 0, curTime in the window that the old
// code would incorrectly skip (between startTime + stageLength and
// startTime + stageOffset + stageLength). Without the fix, this would
// return the NEXT occurrence's start instead of the current one.
TEST_F(FindStartTimeForStageTest, Stage2_DuringLateWindow_SelectsCurrentDate)
{
// Simulate Darkmoon Faire:
// Holiday starts Mar 6 (Friday, building phase)
// Stage 1 (building): 72 hours = 3 days (Mar 6-9)
// Stage 2 (active): 168 hours = 7 days (Mar 9-16)
// Total holiday: Mar 6 - Mar 16 (10 days)
// Next occurrence: Apr 3
uint32_t dates[26] = {};
PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3);
time_t stageOffset = 72 * 3600; // Stage 1 = 72 hours in seconds
uint32_t stageLengthMin = 168 * 60; // Stage 2 = 168 hours in minutes
// curTime = Mar 14 (day 8 of holiday, day 5 of stage 2)
// Old bug: startTime(Mar 6) + 168h = Mar 13, so curTime > that -> SKIP to Apr 3!
// Fixed: startTime(Mar 6) + 72h + 168h = Mar 16, so curTime < that -> correct
time_t curTime = MakeTime(2026, 3, 14, 12);
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
// Should return Mar 6 + stageOffset = Mar 9 (stage 2 start)
EXPECT_EQ(result, MakeTime(2026, 3, 6) + stageOffset);
}
// Stage 2: curTime after entire holiday ends -> selects next occurrence
TEST_F(FindStartTimeForStageTest, Stage2_AfterHolidayEnds_SelectsNextDate)
{
uint32_t dates[26] = {};
PackTwoDates(dates, 2026, 3, 6, 2026, 4, 3);
time_t stageOffset = 72 * 3600;
uint32_t stageLengthMin = 168 * 60;
// curTime = Mar 20 (well past the entire holiday)
time_t curTime = MakeTime(2026, 3, 20);
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
EXPECT_EQ(result, MakeTime(2026, 4, 3) + stageOffset);
}
// No valid dates -> returns 0
TEST_F(FindStartTimeForStageTest, NoDates_ReturnsZero)
{
uint32_t dates[26] = {};
time_t curTime = MakeTime(2026, 6, 1);
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, 0, 168 * 60, curTime);
EXPECT_EQ(result, 0);
}
// All dates in the past -> returns 0
TEST_F(FindStartTimeForStageTest, AllDatesPast_ReturnsZero)
{
uint32_t dates[26] = {};
PackTwoDates(dates, 2026, 1, 5, 2026, 2, 6);
time_t stageOffset = 0;
uint32_t stageLengthMin = 72 * 60;
time_t curTime = MakeTime(2026, 6, 1); // way after both dates
time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime);
EXPECT_EQ(result, 0);
}