revue/widget/
calendar.rs

1//! Calendar widget for date display and selection
2//!
3//! Supports month/year navigation, date selection, range selection,
4//! and custom styling for different date types.
5
6use super::traits::{RenderContext, View, WidgetProps};
7use crate::render::{Cell, Modifier};
8use crate::style::Color;
9use crate::utils::border::render_border;
10use crate::{impl_props_builders, impl_styled_view};
11
12/// Days in a month (accounting for leap years)
13pub fn days_in_month(year: i32, month: u32) -> u32 {
14    match month {
15        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
16        4 | 6 | 9 | 11 => 30,
17        2 => {
18            if is_leap_year(year) {
19                29
20            } else {
21                28
22            }
23        }
24        _ => 0,
25    }
26}
27
28/// Check if year is a leap year
29fn is_leap_year(year: i32) -> bool {
30    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
31}
32
33/// Day of week for the first day of a month (0 = Sunday, 6 = Saturday)
34/// Using Zeller's congruence
35fn first_day_of_month(year: i32, month: u32) -> u32 {
36    let m = if month < 3 {
37        month as i32 + 12
38    } else {
39        month as i32
40    };
41    let y = if month < 3 { year - 1 } else { year };
42    let q = 1i32; // First day of month
43    let k = y % 100;
44    let j = y / 100;
45
46    let h = (q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
47    // Convert from Zeller (0 = Saturday) to standard (0 = Sunday)
48    // Handle negative modulo
49    let h = ((h + 6) % 7 + 7) % 7;
50    h as u32
51}
52
53/// Calendar date
54#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
55pub struct Date {
56    /// Year
57    pub year: i32,
58    /// Month (1-12)
59    pub month: u32,
60    /// Day (1-31)
61    pub day: u32,
62}
63
64impl Date {
65    /// Create a new date
66    pub fn new(year: i32, month: u32, day: u32) -> Self {
67        Self { year, month, day }
68    }
69
70    /// Get today's date (placeholder - returns 2025-01-01)
71    pub fn today() -> Self {
72        // In a real implementation, use chrono or time crate
73        Self::new(2025, 1, 1)
74    }
75
76    /// Check if date is valid
77    pub fn is_valid(&self) -> bool {
78        self.month >= 1
79            && self.month <= 12
80            && self.day >= 1
81            && self.day <= days_in_month(self.year, self.month)
82    }
83
84    /// Day of week (0 = Sunday, 6 = Saturday)
85    pub fn weekday(&self) -> u32 {
86        let first = first_day_of_month(self.year, self.month);
87        (first + self.day - 1) % 7
88    }
89
90    /// Get previous day
91    pub fn prev_day(&self) -> Self {
92        if self.day > 1 {
93            Self::new(self.year, self.month, self.day - 1)
94        } else if self.month > 1 {
95            let prev_month = self.month - 1;
96            let days = days_in_month(self.year, prev_month);
97            Self::new(self.year, prev_month, days)
98        } else {
99            Self::new(self.year - 1, 12, 31)
100        }
101    }
102
103    /// Get next day
104    pub fn next_day(&self) -> Self {
105        let days = days_in_month(self.year, self.month);
106        if self.day < days {
107            Self::new(self.year, self.month, self.day + 1)
108        } else if self.month < 12 {
109            Self::new(self.year, self.month + 1, 1)
110        } else {
111            Self::new(self.year + 1, 1, 1)
112        }
113    }
114
115    /// Subtract n days from this date
116    pub fn subtract_days(&self, n: u32) -> Self {
117        let mut result = *self;
118        for _ in 0..n {
119            result = result.prev_day();
120        }
121        result
122    }
123
124    /// Add n days to this date
125    pub fn add_days(&self, n: u32) -> Self {
126        let mut result = *self;
127        for _ in 0..n {
128            result = result.next_day();
129        }
130        result
131    }
132}
133
134impl Default for Date {
135    fn default() -> Self {
136        Self::new(2025, 1, 1)
137    }
138}
139
140/// Date marker for highlighting specific dates
141#[derive(Clone, Debug)]
142pub struct DateMarker {
143    /// Date to mark
144    pub date: Date,
145    /// Marker color
146    pub color: Color,
147    /// Optional symbol
148    pub symbol: Option<char>,
149}
150
151impl DateMarker {
152    /// Create a new marker
153    pub fn new(date: Date, color: Color) -> Self {
154        Self {
155            date,
156            color,
157            symbol: None,
158        }
159    }
160
161    /// Set symbol
162    pub fn symbol(mut self, symbol: char) -> Self {
163        self.symbol = Some(symbol);
164        self
165    }
166}
167
168/// Calendar display mode
169#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
170pub enum CalendarMode {
171    /// Single month view
172    #[default]
173    Month,
174    /// Year overview (12 months)
175    Year,
176    /// Week view
177    Week,
178}
179
180/// First day of week
181#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
182pub enum FirstDayOfWeek {
183    /// Sunday first (US style)
184    #[default]
185    Sunday,
186    /// Monday first (ISO style)
187    Monday,
188}
189
190/// Calendar widget
191pub struct Calendar {
192    /// Current displayed year
193    year: i32,
194    /// Current displayed month (1-12)
195    month: u32,
196    /// Selected date
197    selected: Option<Date>,
198    /// Selection range end (for range selection)
199    range_end: Option<Date>,
200    /// Display mode
201    mode: CalendarMode,
202    /// First day of week
203    first_day: FirstDayOfWeek,
204    /// Show week numbers
205    show_week_numbers: bool,
206    /// Date markers
207    markers: Vec<DateMarker>,
208    /// Today indicator
209    today: Option<Date>,
210    /// Colors
211    header_fg: Color,
212    header_bg: Option<Color>,
213    day_fg: Color,
214    weekend_fg: Color,
215    selected_fg: Color,
216    selected_bg: Color,
217    today_fg: Color,
218    outside_fg: Color,
219    border_color: Option<Color>,
220    /// Focused (interactive)
221    focused: bool,
222    /// Widget properties
223    props: WidgetProps,
224}
225
226impl Calendar {
227    /// Create a new calendar
228    pub fn new(year: i32, month: u32) -> Self {
229        Self {
230            year,
231            month: month.clamp(1, 12),
232            selected: None,
233            range_end: None,
234            mode: CalendarMode::Month,
235            first_day: FirstDayOfWeek::Sunday,
236            show_week_numbers: false,
237            markers: Vec::new(),
238            today: None,
239            header_fg: Color::CYAN,
240            header_bg: None,
241            day_fg: Color::WHITE,
242            weekend_fg: Color::rgb(150, 150, 150),
243            selected_fg: Color::BLACK,
244            selected_bg: Color::CYAN,
245            today_fg: Color::YELLOW,
246            outside_fg: Color::rgb(80, 80, 80),
247            border_color: None,
248            focused: false,
249            props: WidgetProps::new(),
250        }
251    }
252
253    /// Set selected date
254    pub fn selected(mut self, date: Date) -> Self {
255        self.selected = Some(date);
256        self.year = date.year;
257        self.month = date.month;
258        self
259    }
260
261    /// Set selection range
262    pub fn range(mut self, start: Date, end: Date) -> Self {
263        self.selected = Some(start);
264        self.range_end = Some(end);
265        self
266    }
267
268    /// Set display mode
269    pub fn mode(mut self, mode: CalendarMode) -> Self {
270        self.mode = mode;
271        self
272    }
273
274    /// Set first day of week
275    pub fn first_day(mut self, first: FirstDayOfWeek) -> Self {
276        self.first_day = first;
277        self
278    }
279
280    /// Show week numbers
281    pub fn week_numbers(mut self, show: bool) -> Self {
282        self.show_week_numbers = show;
283        self
284    }
285
286    /// Add date marker
287    pub fn marker(mut self, marker: DateMarker) -> Self {
288        self.markers.push(marker);
289        self
290    }
291
292    /// Add multiple markers
293    pub fn markers(mut self, markers: Vec<DateMarker>) -> Self {
294        self.markers.extend(markers);
295        self
296    }
297
298    /// Set today's date
299    pub fn today(mut self, date: Date) -> Self {
300        self.today = Some(date);
301        self
302    }
303
304    /// Set header color
305    pub fn header_color(mut self, fg: Color) -> Self {
306        self.header_fg = fg;
307        self
308    }
309
310    /// Set header background
311    pub fn header_bg(mut self, bg: Color) -> Self {
312        self.header_bg = Some(bg);
313        self
314    }
315
316    /// Set day color
317    pub fn day_color(mut self, fg: Color) -> Self {
318        self.day_fg = fg;
319        self
320    }
321
322    /// Set weekend color
323    pub fn weekend_color(mut self, fg: Color) -> Self {
324        self.weekend_fg = fg;
325        self
326    }
327
328    /// Set selected colors
329    pub fn selected_color(mut self, fg: Color, bg: Color) -> Self {
330        self.selected_fg = fg;
331        self.selected_bg = bg;
332        self
333    }
334
335    /// Set today color
336    pub fn today_color(mut self, fg: Color) -> Self {
337        self.today_fg = fg;
338        self
339    }
340
341    /// Set border color
342    pub fn border(mut self, color: Color) -> Self {
343        self.border_color = Some(color);
344        self
345    }
346
347    /// Set focused state
348    pub fn focused(mut self, focused: bool) -> Self {
349        self.focused = focused;
350        self
351    }
352
353    /// Navigate to previous month
354    pub fn prev_month(&mut self) {
355        if self.month == 1 {
356            self.month = 12;
357            self.year -= 1;
358        } else {
359            self.month -= 1;
360        }
361    }
362
363    /// Navigate to next month
364    pub fn next_month(&mut self) {
365        if self.month == 12 {
366            self.month = 1;
367            self.year += 1;
368        } else {
369            self.month += 1;
370        }
371    }
372
373    /// Navigate to previous year
374    pub fn prev_year(&mut self) {
375        self.year -= 1;
376    }
377
378    /// Navigate to next year
379    pub fn next_year(&mut self) {
380        self.year += 1;
381    }
382
383    /// Select a date
384    pub fn select(&mut self, date: Date) {
385        self.selected = Some(date);
386    }
387
388    /// Clear selection
389    pub fn clear_selection(&mut self) {
390        self.selected = None;
391        self.range_end = None;
392    }
393
394    /// Get selected date
395    pub fn get_selected(&self) -> Option<Date> {
396        self.selected
397    }
398
399    /// Select next day
400    pub fn select_next_day(&mut self) {
401        if let Some(date) = self.selected {
402            let days = days_in_month(date.year, date.month);
403            if date.day < days {
404                self.selected = Some(Date::new(date.year, date.month, date.day + 1));
405            } else if date.month < 12 {
406                self.selected = Some(Date::new(date.year, date.month + 1, 1));
407                self.month = date.month + 1;
408            } else {
409                self.selected = Some(Date::new(date.year + 1, 1, 1));
410                self.year = date.year + 1;
411                self.month = 1;
412            }
413        } else {
414            self.selected = Some(Date::new(self.year, self.month, 1));
415        }
416    }
417
418    /// Select previous day
419    pub fn select_prev_day(&mut self) {
420        if let Some(date) = self.selected {
421            if date.day > 1 {
422                self.selected = Some(Date::new(date.year, date.month, date.day - 1));
423            } else if date.month > 1 {
424                let prev_month = date.month - 1;
425                let days = days_in_month(date.year, prev_month);
426                self.selected = Some(Date::new(date.year, prev_month, days));
427                self.month = prev_month;
428            } else {
429                let days = days_in_month(date.year - 1, 12);
430                self.selected = Some(Date::new(date.year - 1, 12, days));
431                self.year = date.year - 1;
432                self.month = 12;
433            }
434        } else {
435            self.selected = Some(Date::new(self.year, self.month, 1));
436        }
437    }
438
439    /// Select next week
440    pub fn select_next_week(&mut self) {
441        for _ in 0..7 {
442            self.select_next_day();
443        }
444    }
445
446    /// Select previous week
447    pub fn select_prev_week(&mut self) {
448        for _ in 0..7 {
449            self.select_prev_day();
450        }
451    }
452
453    /// Handle key input
454    pub fn handle_key(&mut self, key: &crate::event::Key) -> bool {
455        use crate::event::Key;
456
457        if !self.focused {
458            return false;
459        }
460
461        match key {
462            Key::Left | Key::Char('h') => {
463                self.select_prev_day();
464                true
465            }
466            Key::Right | Key::Char('l') => {
467                self.select_next_day();
468                true
469            }
470            Key::Up | Key::Char('k') => {
471                self.select_prev_week();
472                true
473            }
474            Key::Down | Key::Char('j') => {
475                self.select_next_week();
476                true
477            }
478            Key::Char('[') => {
479                self.prev_month();
480                true
481            }
482            Key::Char(']') => {
483                self.next_month();
484                true
485            }
486            Key::Char('{') => {
487                self.prev_year();
488                true
489            }
490            Key::Char('}') => {
491                self.next_year();
492                true
493            }
494            _ => false,
495        }
496    }
497
498    /// Check if date is in selection range
499    fn is_in_range(&self, date: &Date) -> bool {
500        match (self.selected, self.range_end) {
501            (Some(start), Some(end)) => {
502                let (start, end) = if start <= end {
503                    (start, end)
504                } else {
505                    (end, start)
506                };
507                date >= &start && date <= &end
508            }
509            _ => false,
510        }
511    }
512
513    /// Get marker for date
514    fn get_marker(&self, date: &Date) -> Option<&DateMarker> {
515        self.markers.iter().find(|m| &m.date == date)
516    }
517
518    /// Get day names
519    fn day_names(&self) -> [&'static str; 7] {
520        match self.first_day {
521            FirstDayOfWeek::Sunday => ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
522            FirstDayOfWeek::Monday => ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
523        }
524    }
525
526    /// Check if day index is weekend
527    fn is_weekend(&self, day_index: u32) -> bool {
528        match self.first_day {
529            FirstDayOfWeek::Sunday => day_index == 0 || day_index == 6,
530            FirstDayOfWeek::Monday => day_index == 5 || day_index == 6,
531        }
532    }
533
534    /// Render month view
535    fn render_month(&self, ctx: &mut RenderContext) {
536        let area = ctx.area;
537        if area.width < 20 || area.height < 8 {
538            return;
539        }
540
541        let has_border = self.border_color.is_some();
542        let start_x = area.x + if has_border { 1 } else { 0 };
543        let start_y = area.y + if has_border { 1 } else { 0 };
544        let week_num_offset: u16 = if self.show_week_numbers { 4 } else { 0 };
545
546        // Draw border if specified
547        if let Some(border_color) = self.border_color {
548            render_border(ctx, area, border_color);
549        }
550
551        // Month name and year header
552        let month_names = [
553            "January",
554            "February",
555            "March",
556            "April",
557            "May",
558            "June",
559            "July",
560            "August",
561            "September",
562            "October",
563            "November",
564            "December",
565        ];
566        let header = format!("{} {}", month_names[(self.month - 1) as usize], self.year);
567        let header_x = start_x + week_num_offset + (20 - header.len() as u16) / 2;
568
569        for (i, ch) in header.chars().enumerate() {
570            let mut cell = Cell::new(ch);
571            cell.fg = Some(self.header_fg);
572            cell.bg = self.header_bg;
573            cell.modifier |= Modifier::BOLD;
574            ctx.buffer.set(header_x + i as u16, start_y, cell);
575        }
576
577        // Navigation arrows
578        if self.focused {
579            let mut left = Cell::new('◀');
580            left.fg = Some(self.header_fg);
581            ctx.buffer.set(start_x + week_num_offset, start_y, left);
582
583            let mut right = Cell::new('▶');
584            right.fg = Some(self.header_fg);
585            ctx.buffer
586                .set(start_x + week_num_offset + 21, start_y, right);
587        }
588
589        // Week header
590        let y = start_y + 2;
591        let day_names = self.day_names();
592
593        if self.show_week_numbers {
594            let mut wk = Cell::new('W');
595            wk.fg = Some(self.header_fg);
596            ctx.buffer.set(start_x, y, wk);
597        }
598
599        for (i, name) in day_names.iter().enumerate() {
600            let x = start_x + week_num_offset + (i as u16) * 3;
601            let is_weekend = self.is_weekend(i as u32);
602
603            for (j, ch) in name.chars().enumerate() {
604                let mut cell = Cell::new(ch);
605                cell.fg = Some(if is_weekend {
606                    self.weekend_fg
607                } else {
608                    self.header_fg
609                });
610                ctx.buffer.set(x + j as u16, y, cell);
611            }
612        }
613
614        // Days
615        let first_day = first_day_of_month(self.year, self.month);
616        let first_day_adjusted = match self.first_day {
617            FirstDayOfWeek::Sunday => first_day,
618            FirstDayOfWeek::Monday => (first_day + 6) % 7,
619        };
620        let days = days_in_month(self.year, self.month);
621
622        let mut day = 1u32;
623        let mut row = 0u32;
624
625        while day <= days {
626            let y = start_y + 3 + row as u16;
627
628            // Week number
629            if self.show_week_numbers {
630                let week_num = self.get_week_number(self.year, self.month, day);
631                let week_str = format!("{:2}", week_num);
632                for (i, ch) in week_str.chars().enumerate() {
633                    let mut cell = Cell::new(ch);
634                    cell.fg = Some(self.outside_fg);
635                    ctx.buffer.set(start_x + i as u16, y, cell);
636                }
637            }
638
639            for col in 0..7u32 {
640                let cell_day = if row == 0 {
641                    if col < first_day_adjusted {
642                        continue;
643                    }
644                    col - first_day_adjusted + 1
645                } else {
646                    row * 7 + col - first_day_adjusted + 1
647                };
648
649                if cell_day < 1 || cell_day > days {
650                    continue;
651                }
652
653                let x = start_x + week_num_offset + col as u16 * 3;
654                let date = Date::new(self.year, self.month, cell_day);
655
656                // Determine styling
657                let is_selected = self.selected == Some(date);
658                let is_in_range = self.is_in_range(&date);
659                let is_today = self.today == Some(date);
660                let is_weekend = self.is_weekend(col);
661                let marker = self.get_marker(&date);
662
663                let (fg, bg, modifier) = if is_selected {
664                    (self.selected_fg, Some(self.selected_bg), Modifier::BOLD)
665                } else if is_in_range {
666                    (
667                        self.selected_fg,
668                        Some(Color::rgb(60, 90, 120)),
669                        Modifier::empty(),
670                    )
671                } else if is_today {
672                    (self.today_fg, None, Modifier::BOLD)
673                } else if let Some(m) = marker {
674                    (m.color, None, Modifier::empty())
675                } else if is_weekend {
676                    (self.weekend_fg, None, Modifier::empty())
677                } else {
678                    (self.day_fg, None, Modifier::empty())
679                };
680
681                // Draw day number
682                let day_str = format!("{:2}", cell_day);
683                for (i, ch) in day_str.chars().enumerate() {
684                    let mut cell = Cell::new(ch);
685                    cell.fg = Some(fg);
686                    cell.bg = bg;
687                    cell.modifier = modifier;
688                    ctx.buffer.set(x + i as u16, y, cell);
689                }
690
691                // Draw marker symbol
692                if let Some(m) = marker {
693                    if let Some(sym) = m.symbol {
694                        let mut cell = Cell::new(sym);
695                        cell.fg = Some(m.color);
696                        ctx.buffer.set(x + 2, y, cell);
697                    }
698                }
699            }
700
701            if row == 0 {
702                day = 8 - first_day_adjusted;
703            } else {
704                day += 7;
705            }
706            row += 1;
707        }
708    }
709
710    /// Get ISO 8601 week number
711    ///
712    /// ISO week rules:
713    /// - Weeks start on Monday
714    /// - Week 1 contains the first Thursday of the year
715    /// - Week numbers range from 1 to 52 or 53
716    fn get_week_number(&self, year: i32, month: u32, day: u32) -> u32 {
717        // Calculate day of year (1-based)
718        let day_of_year = (1..month).map(|m| days_in_month(year, m)).sum::<u32>() + day;
719
720        // Calculate weekday (0=Monday, 6=Sunday) using Zeller's congruence
721        let weekday = {
722            let m = if month < 3 {
723                month as i32 + 12
724            } else {
725                month as i32
726            };
727            let y = if month < 3 { year - 1 } else { year };
728            let k = y % 100;
729            let j = y / 100;
730            let h = (day as i32 + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
731            // Convert from Zeller (0=Sat) to ISO (0=Mon)
732            ((h + 5) % 7) as u32
733        };
734
735        // Calculate ISO week number
736        // Thursday of the same week determines the year for ISO week
737        let thursday_day_of_year = day_of_year as i32 + 3 - weekday as i32;
738
739        if thursday_day_of_year < 1 {
740            // This day belongs to the last week of the previous year
741            return self.get_week_number(year - 1, 12, 31);
742        }
743
744        let days_in_year = if is_leap_year(year) { 366 } else { 365 };
745        if thursday_day_of_year > days_in_year as i32 {
746            // This day belongs to week 1 of the next year
747            return 1;
748        }
749
750        // Calculate week number
751        ((thursday_day_of_year as u32 - 1) / 7) + 1
752    }
753}
754
755impl Default for Calendar {
756    fn default() -> Self {
757        Self::new(2025, 1)
758    }
759}
760
761impl View for Calendar {
762    crate::impl_view_meta!("Calendar");
763
764    fn render(&self, ctx: &mut RenderContext) {
765        match self.mode {
766            CalendarMode::Month => self.render_month(ctx),
767            CalendarMode::Year | CalendarMode::Week => {
768                // Simplified: just render month view for now
769                self.render_month(ctx);
770            }
771        }
772    }
773}
774
775impl_styled_view!(Calendar);
776impl_props_builders!(Calendar);
777
778/// Helper to create a calendar
779pub fn calendar(year: i32, month: u32) -> Calendar {
780    Calendar::new(year, month)
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786    use crate::layout::Rect;
787    use crate::render::Buffer;
788
789    #[test]
790    fn test_calendar_new() {
791        let cal = Calendar::new(2025, 1);
792        assert_eq!(cal.year, 2025);
793        assert_eq!(cal.month, 1);
794    }
795
796    #[test]
797    fn test_calendar_month_clamp() {
798        let cal = Calendar::new(2025, 13);
799        assert_eq!(cal.month, 12);
800
801        let cal = Calendar::new(2025, 0);
802        assert_eq!(cal.month, 1);
803    }
804
805    #[test]
806    fn test_date_new() {
807        let date = Date::new(2025, 6, 15);
808        assert_eq!(date.year, 2025);
809        assert_eq!(date.month, 6);
810        assert_eq!(date.day, 15);
811    }
812
813    #[test]
814    fn test_date_valid() {
815        assert!(Date::new(2025, 1, 1).is_valid());
816        assert!(Date::new(2025, 2, 28).is_valid());
817        assert!(Date::new(2024, 2, 29).is_valid()); // Leap year
818        assert!(!Date::new(2025, 2, 29).is_valid()); // Not leap year
819        assert!(!Date::new(2025, 13, 1).is_valid());
820        assert!(!Date::new(2025, 1, 32).is_valid());
821    }
822
823    #[test]
824    fn test_days_in_month() {
825        assert_eq!(days_in_month(2025, 1), 31);
826        assert_eq!(days_in_month(2025, 2), 28);
827        assert_eq!(days_in_month(2024, 2), 29);
828        assert_eq!(days_in_month(2025, 4), 30);
829    }
830
831    #[test]
832    fn test_leap_year() {
833        assert!(is_leap_year(2024));
834        assert!(!is_leap_year(2025));
835        assert!(is_leap_year(2000));
836        assert!(!is_leap_year(1900));
837    }
838
839    #[test]
840    fn test_calendar_navigation() {
841        let mut cal = Calendar::new(2025, 1);
842
843        cal.next_month();
844        assert_eq!(cal.month, 2);
845
846        cal.prev_month();
847        assert_eq!(cal.month, 1);
848
849        cal.prev_month();
850        assert_eq!(cal.month, 12);
851        assert_eq!(cal.year, 2024);
852
853        cal.next_month();
854        assert_eq!(cal.month, 1);
855        assert_eq!(cal.year, 2025);
856    }
857
858    #[test]
859    fn test_calendar_year_navigation() {
860        let mut cal = Calendar::new(2025, 6);
861
862        cal.next_year();
863        assert_eq!(cal.year, 2026);
864
865        cal.prev_year();
866        assert_eq!(cal.year, 2025);
867    }
868
869    #[test]
870    fn test_calendar_selection() {
871        let mut cal = Calendar::new(2025, 1);
872
873        cal.select(Date::new(2025, 1, 15));
874        assert_eq!(cal.get_selected(), Some(Date::new(2025, 1, 15)));
875
876        cal.clear_selection();
877        assert_eq!(cal.get_selected(), None);
878    }
879
880    #[test]
881    fn test_calendar_select_next_day() {
882        let mut cal = Calendar::new(2025, 1).selected(Date::new(2025, 1, 31));
883
884        cal.select_next_day();
885        assert_eq!(cal.get_selected(), Some(Date::new(2025, 2, 1)));
886        assert_eq!(cal.month, 2);
887    }
888
889    #[test]
890    fn test_calendar_select_prev_day() {
891        let mut cal = Calendar::new(2025, 2).selected(Date::new(2025, 2, 1));
892
893        cal.select_prev_day();
894        assert_eq!(cal.get_selected(), Some(Date::new(2025, 1, 31)));
895        assert_eq!(cal.month, 1);
896    }
897
898    #[test]
899    fn test_date_marker() {
900        let marker = DateMarker::new(Date::new(2025, 1, 1), Color::RED).symbol('★');
901
902        assert_eq!(marker.date, Date::new(2025, 1, 1));
903        assert_eq!(marker.color, Color::RED);
904        assert_eq!(marker.symbol, Some('★'));
905    }
906
907    #[test]
908    fn test_calendar_range() {
909        let cal = Calendar::new(2025, 1).range(Date::new(2025, 1, 10), Date::new(2025, 1, 20));
910
911        assert!(cal.is_in_range(&Date::new(2025, 1, 15)));
912        assert!(!cal.is_in_range(&Date::new(2025, 1, 5)));
913    }
914
915    #[test]
916    fn test_calendar_render() {
917        let mut buffer = Buffer::new(30, 12);
918        let area = Rect::new(0, 0, 30, 12);
919        let mut ctx = RenderContext::new(&mut buffer, area);
920
921        let cal = Calendar::new(2025, 1)
922            .selected(Date::new(2025, 1, 15))
923            .today(Date::new(2025, 1, 10));
924
925        cal.render(&mut ctx);
926        // Smoke test - renders without panic
927    }
928
929    #[test]
930    fn test_calendar_with_border() {
931        let mut buffer = Buffer::new(30, 12);
932        let area = Rect::new(0, 0, 30, 12);
933        let mut ctx = RenderContext::new(&mut buffer, area);
934
935        let cal = Calendar::new(2025, 1).border(Color::WHITE);
936        cal.render(&mut ctx);
937
938        assert_eq!(buffer.get(0, 0).unwrap().symbol, '┌');
939    }
940
941    #[test]
942    fn test_calendar_first_day() {
943        let cal_sun = Calendar::new(2025, 1).first_day(FirstDayOfWeek::Sunday);
944        let cal_mon = Calendar::new(2025, 1).first_day(FirstDayOfWeek::Monday);
945
946        assert_eq!(cal_sun.day_names()[0], "Su");
947        assert_eq!(cal_mon.day_names()[0], "Mo");
948    }
949
950    #[test]
951    fn test_first_day_of_month() {
952        // January 1, 2025 is Wednesday
953        assert_eq!(first_day_of_month(2025, 1), 3);
954    }
955
956    #[test]
957    fn test_calendar_helper() {
958        let cal = calendar(2025, 6);
959        assert_eq!(cal.year, 2025);
960        assert_eq!(cal.month, 6);
961    }
962
963    #[test]
964    fn test_iso_week_number() {
965        let cal = Calendar::new(2025, 1);
966
967        // 2025-01-01 is Wednesday, ISO week 1
968        assert_eq!(cal.get_week_number(2025, 1, 1), 1);
969
970        // 2025-01-06 is Monday, still week 1
971        assert_eq!(cal.get_week_number(2025, 1, 6), 2);
972
973        // 2024-12-30 is Monday, week 1 of 2025
974        assert_eq!(cal.get_week_number(2024, 12, 30), 1);
975
976        // 2024-12-28 is Saturday, week 52 of 2024
977        assert_eq!(cal.get_week_number(2024, 12, 28), 52);
978    }
979
980    #[test]
981    fn test_iso_week_number_edge_cases() {
982        let cal = Calendar::new(2020, 1);
983
984        // 2020-01-01 is Wednesday, ISO week 1
985        assert_eq!(cal.get_week_number(2020, 1, 1), 1);
986
987        // 2019-12-30 is Monday, week 1 of 2020
988        assert_eq!(cal.get_week_number(2019, 12, 30), 1);
989
990        // 2020-12-31 is Thursday, week 53 of 2020
991        assert_eq!(cal.get_week_number(2020, 12, 31), 53);
992    }
993}