// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone

using AngleSharp.Dom;

namespace UnitTest.Components;

public class DateTimePickerTest : BootstrapBlazorTestBase
{
    #region DateTimePicker
    [Fact]
    public void Color_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.AutoToday, true);
            pb.Add(a => a.Value, DateTime.MinValue);
            pb.Add(a => a.Color, Color.Primary);
        });
        cut.Contains("border-primary");
    }

    [Fact]
    public void AutoToday_DateTime()
    {
        // 设置为 最小值或者 null 时 当 AutoToday 为 true 时自动设置为当前时间
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.AutoToday, true);
            pb.Add(a => a.Value, DateTime.MinValue);
        });
        Assert.Equal(DateTime.Today, cut.Instance.Value);

        var input = cut.Find(".datetime-picker-input");
        Assert.Equal(DateTime.Today.ToString("yyyy-MM-dd"), input.GetAttribute("value"));

        cut.Render(pb =>
        {
            pb.Add(a => a.AutoToday, false);
            pb.Add(a => a.Value, DateTime.MinValue);
        });
        Assert.Equal(DateTime.MinValue, cut.Instance.Value);
        input = cut.Find(".datetime-picker-input");
        Assert.Equal(DateTime.MinValue.ToString("yyyy-MM-dd"), input.GetAttribute("value"));
    }

    [Fact]
    public void AutoToday_DateTimeOffset()
    {
        // 设置为 最小值或者 null 时 当 AutoToday 为 true 时自动设置为当前时间
        var cut = Context.Render<DateTimePicker<DateTimeOffset>>(pb =>
        {
            pb.Add(a => a.AutoToday, true);
            pb.Add(a => a.Value, DateTimeOffset.MinValue);
        });
        Assert.Equal(new DateTimeOffset(DateTime.Today), cut.Instance.Value);

        var input = cut.Find(".datetime-picker-input");
        Assert.Equal($"{DateTimeOffset.Now:yyyy-MM-dd}", input.GetAttribute("value"));

        cut.Render(pb =>
        {
            pb.Add(a => a.AutoToday, false);
            pb.Add(a => a.Value, DateTimeOffset.MinValue);
        });
        Assert.Equal(DateTimeOffset.MinValue, cut.Instance.Value);
        input = cut.Find(".datetime-picker-input");
        Assert.Equal(DateTimeOffset.MinValue.ToString("yyyy-MM-dd"), input.GetAttribute("value"));
    }

    [Fact]
    public void DisplayMinValueAsEmpty_NullableDateTime()
    {
        var cut = Context.Render<DateTimePicker<DateTime?>>(pb =>
        {
            pb.Add(a => a.Value, null);
        });
        Assert.Null(cut.Instance.Value);

        var input = cut.Find(".datetime-picker-input");
        Assert.Equal("", input.GetAttribute("value"));

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTime.MinValue);
            pb.Add(a => a.DisplayMinValueAsEmpty, true);
        });
        Assert.Null(cut.Instance.Value);
        input = cut.Find(".datetime-picker-input");
        Assert.Equal("", input.GetAttribute("value"));

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTime.MinValue);
            pb.Add(a => a.DisplayMinValueAsEmpty, false);
        });
        Assert.Equal(DateTime.MinValue, cut.Instance.Value);
        input = cut.Find(".datetime-picker-input");
        Assert.Equal($"{DateTime.MinValue:yyyy-MM-dd}", input.GetAttribute("value"));
    }

    [Fact]
    public void DisplayMinValueAsEmpty_NullableDateTimeOffset()
    {
        var cut = Context.Render<DateTimePicker<DateTimeOffset?>>(pb =>
        {
            pb.Add(a => a.Value, null);
        });
        Assert.Null(cut.Instance.Value);

        var input = cut.Find(".datetime-picker-input");
        Assert.Equal("", input.GetAttribute("value"));

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTimeOffset.MinValue);
        });
        Assert.Null(cut.Instance.Value);
        input = cut.Find(".datetime-picker-input");
        Assert.Equal("", input.GetAttribute("value"));

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTimeOffset.MinValue);
            pb.Add(a => a.DisplayMinValueAsEmpty, false);
        });
        Assert.Equal(DateTimeOffset.MinValue, cut.Instance.Value);
        input = cut.Find(".datetime-picker-input");
        Assert.Equal($"{DateTimeOffset.MinValue:yyyy-MM-dd}", input.GetAttribute("value"));
    }

    [Fact]
    public async Task OnClear_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime?>>(pb =>
        {
            pb.Add(a => a.AutoToday, false);
            pb.Add(a => a.DisplayMinValueAsEmpty, false);
            pb.Add(a => a.Value, null);
        });

        // 点击 0001-01-01 单元格
        var cell = cut.Find(".current .cell");
        await cut.InvokeAsync(() => cell.Click());
        // 文本框内容
        var input = cut.Find(".datetime-picker-input");
        Assert.Equal($"{DateTime.MinValue:yyyy-MM-dd}", input.GetAttribute("value"));

        // 点击清空按钮
        var clear = cut.Find(".picker-panel-footer button");
        await cut.InvokeAsync(() => clear.Click());

        // 文本框内容 为 ""
        input = cut.Find(".datetime-picker-input");
        Assert.Equal("", input.GetAttribute("value"));

        // 设置 DisplayMinValueAsEmpty="true" MinValue 自动为 ToDay
        cut.Render(pb =>
        {
            pb.Add(a => a.DisplayMinValueAsEmpty, true);
        });
        cell = cut.Find(".current .cell");
        await cut.InvokeAsync(() => cell.Click());

        // 文本框内容
        input = cut.Find(".datetime-picker-input");
        Assert.Equal($"{DateTime.Today:yyyy-MM-dd}", input.GetAttribute("value"));

        // 点击清空按钮
        clear = cut.Find(".picker-panel-footer button");
        await cut.InvokeAsync(() => clear.Click());

        // 文本框内容 为 ""
        input = cut.Find(".datetime-picker-input");
        Assert.Equal("", input.GetAttribute("value"));

        // 设置最小时间值
        cut.Render(pb =>
        {
            pb.Add(a => a.MinValue, DateTime.Today);
            pb.Add(a => a.Value, null);
        });
        clear = cut.Find(".picker-panel-footer button");
        await cut.InvokeAsync(() => clear.Click());
    }

    [Fact]
    public void ShowSiderBar_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(builder => builder.Add(a => a.ShowSidebar, true));

        var ele = cut.Find(".picker-panel-sidebar");
        Assert.NotNull(ele);
    }

    [Fact]
    public void Placement_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(builder => builder.Add(a => a.Placement, Placement.Top));

        Assert.Contains("data-bs-placement=\"top\"", cut.Markup);
    }

    [Fact]
    public void DateTimeFormat_OK()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(builder =>
        {
            builder.Add(a => a.Value, DateTime.Now);
            builder.Add(a => a.DateFormat, "yyyy/MM/dd");
        });

        var value = cut.Find(".datetime-picker-input").GetAttribute("value");

        Assert.Equal(value, DateTime.Now.ToString("yyyy/MM/dd"));
    }

    [Fact]
    public void MaxValue_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(builder => builder.Add(a => a.MaxValue, DateTime.Today.AddDays(1)));

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.Date);
            pb.Add(a => a.MaxValue, DateTime.Today.AddDays(-1));
            pb.Add(a => a.Value, DateTime.Today);
        });
        Assert.Equal(DateTime.Today.AddDays(-1), cut.Instance.Value);

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            pb.Add(a => a.MaxValue, DateTime.Today.AddDays(-1));
            pb.Add(a => a.Value, DateTime.Today);
        });
        Assert.Equal(DateTime.Today.AddDays(-1), cut.Instance.Value.Date);

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            pb.Add(a => a.MaxValue, DateTime.Today.AddDays(1));
            pb.Add(a => a.Value, DateTime.Today);
        });
        Assert.Equal(DateTime.Today, cut.Instance.Value.Date);

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            pb.Add(a => a.MaxValue, null);
            pb.Add(a => a.Value, DateTime.Today);
        });
        Assert.Equal(DateTime.Today, cut.Instance.Value.Date);
    }

    [Fact]
    public void MinValue_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(builder =>
        {
            builder.Add(a => a.MinValue, DateTime.Today.AddDays(-1));
        });

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.Date);
            pb.Add(a => a.MinValue, DateTime.Today.AddDays(1));
            pb.Add(a => a.Value, DateTime.Today);
        });
        Assert.Equal(DateTime.Today.AddDays(1), cut.Instance.Value);

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            pb.Add(a => a.MinValue, DateTime.Today.AddDays(1));
            pb.Add(a => a.Value, DateTime.Today);
        });
        Assert.Equal(DateTime.Today.AddDays(1), cut.Instance.Value.Date);

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            pb.Add(a => a.MinValue, DateTime.Today.AddDays(-1));
            pb.Add(a => a.Value, DateTime.Today);
        });
        Assert.Equal(DateTime.Today, cut.Instance.Value.Date);

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            pb.Add(a => a.MinValue, null);
            pb.Add(a => a.Value, DateTime.Today);
        });
        Assert.Equal(DateTime.Today, cut.Instance.Value.Date);
    }

    [Fact]
    public void OnTimeChanged_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(builder =>
        {
            builder.Add(a => a.Value, new DateTime(2023, 10, 1, 1, 0, 0));
            builder.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
        });

        var panel = cut.FindComponent<ClockPicker>();
        cut.InvokeAsync(() => panel.Instance.SetTime(0, 0, 0));
        // 点击确定
        var buttons = cut.FindAll(".picker-panel-footer button");
        cut.InvokeAsync(() => buttons[1].Click());

        var body = cut.FindComponent<DatePickerBody>();
        Assert.Equal(TimeSpan.Zero, body.Instance.Value.TimeOfDay);
    }

    [Fact]
    public void SwitchTimeView_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(builder =>
        {
            builder.Add(a => a.Value, new DateTime(2023, 10, 1, 1, 0, 0));
            builder.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            builder.Add(a => a.PickTimeMode, PickTimeMode.Clock);
        });

        var labels = cut.FindAll(".picker-panel-header-label");
        cut.InvokeAsync(() => labels[2].Click());

        cut.Contains("picker-panel-body-main-wrapper is-open");
    }

    [Fact]
    public void ValidateForm_Ok()
    {
        var foo = new Foo();
        var cut = Context.Render<ValidateForm>(pb =>
        {
            pb.Add(a => a.Model, foo);
            pb.AddChildContent<DateTimePicker<DateTime?>>(pb =>
            {
                pb.Add(a => a.Value, foo.DateTime);
                pb.Add(a => a.ValueExpression, foo.GenerateValueExpression(nameof(Foo.DateTime), typeof(DateTime?)));
            });
        });
        cut.Contains("class=\"form-label\"");
    }

    [Fact]
    public void NotDateTime_Error()
    {
        Assert.ThrowsAny<InvalidOperationException>(() =>
        {
            Context.Render<DateTimePicker<int>>();
        });
    }

    [Fact]
    public async Task PickTimeMode_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            builder.Add(a => a.PickTimeMode, PickTimeMode.Dropdown);
            builder.Add(a => a.ShowFooter, true);
            builder.Add(a => a.Value, DateTime.Today.AddDays(-1));
            builder.Add(a => a.TimeFormat, "hh\\:mm");
        });

        cut.Contains("picker-panel-time");

        // 点击时间选择器
        var input = cut.Find(".picker-panel-time .form-control");
        await cut.InvokeAsync(() => input.Click());
        cut.Contains("picker-panel-time show");

        // 点击时间选择器下拉框中的确定按钮
        var picker = cut.FindComponent<TimePicker>();
        await cut.InvokeAsync(() => picker.Instance.OnClose!());

        var ts = DateTime.Now.TimeOfDay;
        await cut.InvokeAsync(() => picker.Instance.OnConfirm!(ts));
        Assert.Contains(ts.ToString("hh\\:mm"), cut.Markup);

        cut.Render(pb =>
        {
            pb.Add(a => a.TimeFormat, null);
        });
    }

    [Fact]
    public void IsButton_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.IsButton, true);
            pb.Add(a => a.ButtonColor, Color.Danger);
            pb.Add(a => a.PickerButtonText, "Pick DateTime");
        });
        cut.Contains("btn dropdown-toggle btn-danger");

        cut.Render(pb =>
        {
            pb.Add(a => a.IsButton, false);
        });
        cut.DoesNotContain("btn dropdown-toggle btn-danger");
        cut.Contains("dropdown-toggle form-control datetime-picker-input");
    }
    #endregion

    #region DatePicker
    [Fact]
    public void DateFormat_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.ShowFooter, false);
            builder.Add(a => a.Value, DateTime.Today.AddDays(-1));
            builder.Add(a => a.DateFormat, "yyyy/MM/dd");
        });

        cut.InvokeAsync(() => cut.Find(".current .cell").Click());
    }

    [Fact]
    public void ShowIcon_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.ShowIcon, false);
        });
        cut.DoesNotContain("datetime-picker-bar");
    }

    [Fact]
    public void Format_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.DateTimeFormat, "yyyy-MM-dd HH:mm:ss");
        });

        var body = cut.FindComponent<DatePickerBody>();
        Assert.Equal("yyyy-MM-dd", body.Instance.DateFormat);
    }

    [Fact]
    public void ShowLunar_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.ShowLunar, true);
            pb.Add(a => a.Value, new DateTime(2024, 6, 10));
        });

        cut.Contains("三十");
        cut.Contains("廿一");
        cut.Contains("初二");

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, new DateTime(2023, 3, 22));
        });
        cut.Contains("闰二月");
    }

    [Fact]
    public void ShowSolarTerm_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.ShowLunar, true);
            pb.Add(a => a.ShowSolarTerm, true);
            pb.Add(a => a.Value, new DateTime(2024, 3, 5));
        });
        cut.Contains("惊蛰");

        cut.Render(pb =>
        {
            pb.Add(a => a.ShowFestivals, true);
        });
        cut.Contains("妇女节");

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, new DateTime(2023, 2, 20));
        });
        cut.Contains("二月");

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, new DateTime(2023, 3, 22));
        });
        cut.Contains("闰二月");
    }

    [Fact]
    public void ShowHolidays_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.ShowHolidays, true);
            pb.Add(a => a.Value, new DateTime(2024, 3, 5));
        });
        cut.DoesNotContain("休");
    }

    [Fact]
    public void ShowWorkdays_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.ShowHolidays, true);
            pb.Add(a => a.Value, new DateTime(2024, 4, 7));
        });
        cut.DoesNotContain("班");
    }

    [Fact]
    public void ShowHolidays_Custom()
    {
        var context = new BunitContext();
        context.JSInterop.Mode = JSRuntimeMode.Loose;

        var services = context.Services;
        services.AddBootstrapBlazor();
        services.AddSingleton<ICalendarHolidays, MockCalendarHolidayService>();

        var cut = context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.ShowHolidays, true);
            pb.Add(a => a.Value, new DateTime(2024, 3, 17));
        });
        cut.Contains("休");
    }

    [Fact]
    public void ShowWorkdays_Custom()
    {
        var context = new BunitContext();
        context.JSInterop.Mode = JSRuntimeMode.Loose;

        var services = context.Services;
        services.AddBootstrapBlazor();
        services.AddSingleton<ICalendarHolidays, MockCalendarHolidayService>();

        var cut = context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.ShowHolidays, true);
            pb.Add(a => a.Value, new DateTime(2024, 4, 7));
        });
        cut.Contains("班");
    }

    class MockCalendarHolidayService : ICalendarHolidays
    {
        public bool IsHoliday(DateTime dt) => dt == new DateTime(2024, 3, 17);
        public bool IsWorkday(DateTime dt) => dt == new DateTime(2024, 4, 7);
    }

    [Fact]
    public async Task TriggerHideCallback_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.DayTemplate, dt => builder =>
            {
                builder.AddContent(0, "day-template");
            });
        });
        await cut.InvokeAsync(() => cut.Instance.TriggerHideCallback());
    }

    [Fact]
    public void DayTemplate_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.DayTemplate, dt => builder =>
            {
                builder.AddContent(0, "day-template");
            });
        });

        cut.Contains("day-template");
    }

    [Fact]
    public void DayDisabledTemplate_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.DayDisabledTemplate, dt => builder =>
            {
                builder.AddContent(0, "day-disabled-template");
            });
            pb.Add(a => a.MinValue, new DateTime(2024, 3, 7));
            pb.Add(a => a.MaxValue, new DateTime(2024, 3, 17));
            pb.Add(a => a.Value, new DateTime(2024, 3, 10));
        });

        cut.Contains("day-disabled-template");
        cut.Render(pb =>
        {
            pb.Add(a => a.ShowLunar, true);
            pb.Add(a => a.ShowSolarTerm, true);
        });
        cut.Render(pb =>
        {
            pb.Add(a => a.ShowFestivals, true);
        });
    }

    [Fact]
    public void DatePickerViewModel_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.Value, DateTime.Now);
            builder.Add(a => a.ViewMode, DatePickerViewMode.Year);
        });

        var labels = cut.FindAll(".picker-panel-header-label");
        Assert.Equal(GetYearPeriod(), labels[0].TextContent);

        // 上一年
        var buttons = cut.FindAll(".picker-panel-header button");
        cut.InvokeAsync(() => buttons[0].Click());

        // 下一年
        cut.InvokeAsync(() => buttons[3].Click());

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.Month);
            pb.Add(a => a.Value, GetToday());
        });

        DateTime GetToday()
        {
            var buffer = 1;
            var month = DateTime.Today.Month + buffer;
            if (month > 6)
            {
                buffer = -1;
            }
            return DateTime.Today.AddMonths(month);
        }

        string GetYearPeriod()
        {
            var start = DateTime.Today.AddYears(0 - DateTime.Today.Year % 20).Year;
            return string.Format("{0} 年 - {1} 年", start, start + 19);
        }
    }

    [Fact]
    public async Task IsYearOverflow_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.Value, DateTime.MinValue.AddDays(1));
            builder.Add(a => a.ViewMode, DatePickerViewMode.Year);
        });

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTime.MaxValue);
            pb.Add(a => a.ViewMode, DatePickerViewMode.Date);
        });

        var buttons = cut.FindAll(".picker-panel-header button");
        // 下一月
        await cut.InvokeAsync(() => buttons[2].Click());
        // 下一年
        await cut.InvokeAsync(() => buttons[3].Click());

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTime.MinValue);
            pb.Add(a => a.ViewMode, DatePickerViewMode.Date);
        });

        buttons = cut.FindAll(".picker-panel-header button");
        // 上一年
        await cut.InvokeAsync(() => buttons[0].Click());
        // 上一月
        await cut.InvokeAsync(() => buttons[1].Click());
    }

    [Fact]
    public void IsDayOverflow()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.Value, DateTime.MaxValue.AddDays(-1));
            builder.Add(a => a.ViewMode, DatePickerViewMode.Date);
        });
    }

    [Fact]
    public void ShowSidebar_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.Value, DateTime.Now);
            builder.Add(a => a.ShowSidebar, true);
        });

        var ele = cut.Find(".picker-panel-sidebar");
        Assert.NotNull(ele);

        var cells = cut.FindAll(".cell");
        cut.InvokeAsync(() => cells[0].Click());
        cut.InvokeAsync(() => cells[1].Click());
        cut.InvokeAsync(() => cells[2].Click());
    }

    [Fact]
    public void ShowButtons_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.Value, DateTime.Now);
        });

        var buttons = cut.FindAll(".picker-panel-header button");

        // 上一年
        cut.InvokeAsync(() => buttons[0].Click());
        var labels = cut.FindAll(".picker-panel-header-label");
        Assert.Equal(DateTime.Today.AddYears(-1).Year.ToString() + " 年", labels[0].TextContent);

        // 下一年
        cut.InvokeAsync(() => buttons[3].Click());
        labels = cut.FindAll(".picker-panel-header-label");
        Assert.Equal(DateTime.Today.Year.ToString() + " 年", labels[0].TextContent);

        // 上一月
        cut.InvokeAsync(() => buttons[1].Click());
        labels = cut.FindAll(".picker-panel-header-label");
        Assert.Equal(DateTime.Today.AddMonths(-1).Month.ToString() + " 月", labels[1].TextContent);

        // 下一月
        cut.InvokeAsync(() => buttons[2].Click());
        labels = cut.FindAll(".picker-panel-header-label");
        Assert.Equal(DateTime.Today.Month.ToString() + " 月", labels[1].TextContent);

        // 年视图
        labels = cut.FindAll(".picker-panel-header-label");
        cut.InvokeAsync(() => labels[0].Click());
        cut.Contains("class=\"year-table\"");

        cut.InvokeAsync(() => cut.Find(".year-table .current.today .cell").Click());
    }

    [Fact]
    public void NotShowButtons_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.Value, DateTime.Now);
            builder.Add(a => a.ShowLeftButtons, false);
        });
        Assert.DoesNotContain("fa-solid fa-angles-left", cut.Find(".picker-panel-header").ToMarkup());

        cut.Render(pb => pb.Add(a => a.ShowRightButtons, false));
        Assert.DoesNotContain("fa-solid fa-angles-right", cut.Find(".picker-panel-header").ToMarkup());
    }

    [Fact]
    public void MonthView_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.Value, DateTime.Now);
        });
        var labels = cut.FindAll(".picker-panel-header-label");
        cut.InvokeAsync(() => labels[1].Click());
        cut.Contains("class=\"month-table\"");

        cut.InvokeAsync(() => cut.Find(".month-table .current.today .cell").Click());
    }

    [Fact]
    public void IsDisabledCell_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.Value, DateTime.Now);
            builder.Add(a => a.MinValue, DateTime.Today.AddDays(-1));
        });

        cut.Render(pb =>
        {
            pb.Add(a => a.MinValue, null);
            pb.Add(a => a.MaxValue, DateTime.Today.AddDays(7));
        });
    }

    [Fact]
    public void ShowFooter_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.Value, DateTime.Now);
            builder.Add(a => a.ShowFooter, true);
            builder.Add(a => a.ShowClearButton, true);
        });

        var ele = cut.Find(".picker-panel-footer");
        Assert.NotNull(ele);

        var buttons = cut.FindAll(".picker-panel-footer button");

        // Click Now
        cut.InvokeAsync(() => buttons[1].Click());
        Assert.Equal(DateTime.Today, cut.Instance.Value.Date);

        // Click Clear Button
        cut.InvokeAsync(() => buttons[0].Click());
        Assert.Equal(DateTime.MinValue, cut.Instance.Value);

        cut.Render(pb =>
        {
            pb.Add(a => a.ShowFooter, false);
        });
        cut.DoesNotContain("picker-panel-footer");
    }

    [Fact]
    public void ClickNowButton_Ok()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            builder.Add(a => a.ShowFooter, true);
            builder.Add(a => a.Value, DateTime.Today.AddDays(-10));
        });
        var button = cut.Find(".is-now");
        cut.InvokeAsync(() => button.Click());

        // 有最小值 无 Now 按钮
        cut.Render(pb =>
        {
            pb.Add(a => a.MinValue, DateTime.Today.AddDays(10));
        });
        cut.DoesNotContain(".picker-panel-footer .is-now");
    }

    [Fact]
    public void ClickDay_Validate()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            builder.Add(a => a.ShowFooter, false);
            builder.Add(a => a.Value, DateTime.Today);
        });

        // 不显示 Footer 点击日期直接确认 OnConfirm
        var cell = cut.Find(".current.today .cell");
        cut.InvokeAsync(() => cell.Click());
    }

    [Fact]
    public void Validate_Ok()
    {
        // (!MinValue.HasValue || Value >= MinValue.Value) && (!MaxValue.HasValue || Value <= MaxValue.Value)
        var cut = Context.Render<DatePickerBody>(pb =>
        {
            pb.Add(a => a.ShowFooter, true);
            pb.Add(a => a.MinValue, DateTime.Now.AddDays(-1));
            pb.Add(a => a.Value, DateTime.Now);
        });
        var button = cut.Find(".is-confirm");
        cut.InvokeAsync(() => button.Click());

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTime.Now.AddDays(-2));
        });
        button = cut.Find(".is-confirm");
        cut.InvokeAsync(() => button.Click());

        cut.Render(pb =>
        {
            pb.Add(a => a.MinValue, null);
            pb.Add(a => a.MaxValue, DateTime.Now.AddDays(1));
            pb.Add(a => a.Value, DateTime.Now);
        });
        button = cut.Find(".is-confirm");
        cut.InvokeAsync(() => button.Click());

        cut.Render(pb =>
        {
            pb.Add(a => a.MaxValue, DateTime.Now.AddDays(1));
            pb.Add(a => a.Value, DateTime.Now.AddDays(2));
        });
        button = cut.Find(".is-confirm");
        cut.InvokeAsync(() => button.Click());
    }

    [Fact]
    public void IsDisabled_Ok()
    {
        // MinValue != null && MaxValue != null && (day < MinValue || day > MaxValue)
        var cut = Context.Render<DatePickerBody>(pb =>
        {
            pb.Add(a => a.MinValue, new DateTime(2022, 02, 15));
            pb.Add(a => a.MaxValue, new DateTime(2022, 02, 17));
            pb.Add(a => a.Value, new DateTime(2022, 02, 16));
        });

        cut.Render(pb =>
        {
            pb.Add(a => a.MinValue, null);
        });
    }
    #endregion

    #region TimeCell
    [Theory]
    [InlineData(TimePickerCellViewMode.Hour)]
    [InlineData(TimePickerCellViewMode.Minute)]
    [InlineData(TimePickerCellViewMode.Second)]
    public async Task TimeCell_Up(TimePickerCellViewMode viewMode)
    {
        var valueChanged = false;
        var cut = Context.Render<TimePickerCell>(pb =>
        {
            pb.Add(a => a.ViewMode, viewMode);
            pb.Add(a => a.Value, new TimeSpan(6, 6, 6));
            pb.Add(a => a.ValueChanged, v =>
            {
                valueChanged = true;
            });
        });
        var i = cut.Find(".time-spinner-arrow.fa-angle-up");
        await cut.InvokeAsync(() => i.Click());
        Assert.True(valueChanged);

        i = cut.Find(".time-spinner-arrow.fa-angle-down");
        await cut.InvokeAsync(() => i.Click());
        Assert.True(valueChanged);
    }

    [Fact]
    public async Task TimeCell_OverDay()
    {
        var ts = new TimeSpan(23, 59, 59);
        var cut = Context.Render<TimePickerCell>(pb =>
        {
            pb.Add(a => a.ViewMode, TimePickerCellViewMode.Second);
            pb.Add(a => a.Value, ts);
            pb.Add(a => a.ValueChanged, v =>
            {
                ts = v;
            });
        });

        var i = cut.Find(".time-spinner-arrow.fa-angle-down");
        await cut.InvokeAsync(() => i.Click());
        Assert.Equal(0, ts.Days);
        Assert.Equal(TimeSpan.Zero, ts);

        i = cut.Find(".time-spinner-arrow.fa-angle-up");
        await cut.InvokeAsync(() => i.Click());
        Assert.Equal(new TimeSpan(23, 59, 59), ts);
    }
    #endregion

    [Fact]
    public async Task ViewMode_DateTime()
    {
        var cut = Context.Render<DatePickerBody>(builder =>
        {
            builder.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            builder.Add(a => a.Value, DateTime.Today.AddDays(-10));
        });

        // 选择年
        var buttons = cut.FindAll(".picker-panel-header button");
        await cut.InvokeAsync(() => buttons[2].Click());

        var button = cut.Find(".year-table tbody .cell");
        await cut.InvokeAsync(() => button.Click());

        button = cut.Find(".month-table tbody .cell");
        await cut.InvokeAsync(() => button.Click());

        cut.Contains("class=\"date-table\"");
    }

    [Fact]
    public async Task AutoClose_OK()
    {
        DateTime val = DateTime.MinValue;
        var cut = Context.Render<DateTimePicker<DateTime>>(builder =>
        {
            builder.Add(a => a.Value, DateTime.Today);
            builder.Add(a => a.AutoClose, false);
            builder.Add(a => a.OnValueChanged, dt =>
            {
                val = dt;
                return Task.CompletedTask;
            });
        });
        var input = cut.Find(".datetime-picker-input");
        Assert.Equal($"{DateTime.Today:yyyy-MM-dd}", input.GetAttribute("value"));

        var button = cut.FindAll(".picker-panel-content .cell").First(i => i.TextContent == $"{DateTime.Today.Day}");
        await cut.InvokeAsync(() => button.Click());
        Assert.Equal(val, DateTime.MinValue);

        cut.Render(pb =>
        {
            pb.Add(a => a.AutoClose, true);
        });
        button = cut.FindAll(".picker-panel-content .cell").First(i => i.TextContent == $"{DateTime.Today.Day}");
        await cut.InvokeAsync(() => button.Click());
    }

    [Fact]
    public void SidebarTemplate_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.ShowSidebar, true);
            pb.Add(a => a.SidebarTemplate, new RenderFragment<Func<DateTime, Task>>(cb => builder =>
            {
                builder.AddContent(0, "test-sidebar-template");
            }));
        });
        cut.Contains("test-sidebar-template");
    }

    [Fact]
    public void FirstDayOfWeek_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.FirstDayOfWeek, DayOfWeek.Monday);
            pb.Add(a => a.Value, new DateTime(2025, 02, 20));
        });

        var labels = cut.FindAll(".date-table tbody > tr:first-child > th");
        Assert.Equal("一", labels[0].TextContent);
        Assert.Equal("日", labels[6].TextContent);
    }

    [Fact]
    public void GetSafeYearDateTime_Ok()
    {
        Assert.True(MockDateTimePicker.GetSafeYearDateTime_Ok());
    }

    [Fact]
    public void PickerBodyShowFooter_Ok()
    {
        var confirm = false;
        var cut = Context.Render<DatePickerBody>(pb =>
        {
            pb.Add(a => a.ShowFooter, true);
            pb.Add(a => a.AutoClose, true);
            pb.Add(a => a.ViewMode, DatePickerViewMode.Month);
            pb.Add(a => a.OnConfirm, () =>
            {
                confirm = true;
                return Task.CompletedTask;
            });
        });

        // 设定为 月视图 切换 日视图时自动关闭触发 Confirm
        var cell = cut.Find(".month-table span.cell");
        cut.InvokeAsync(() => cell.Click());
        Assert.True(confirm);

        // 设置 AutoClose = false 不会触发 confirm
        confirm = false;
        cut.Render(pb =>
        {
            pb.Add(a => a.AutoClose, false);
        });
        cut.InvokeAsync(() => cell.Click());
        Assert.False(confirm);
    }

    [Fact]
    public void OnClickShortLink_Ok()
    {
        var confirm = false;
        var cut = Context.Render<DatePickerBody>(pb =>
        {
            pb.Add(a => a.ShowFooter, false);
            pb.Add(a => a.AutoClose, true);
            pb.Add(a => a.ShowSidebar, true);
            pb.Add(a => a.ViewMode, DatePickerViewMode.Month);
            pb.Add(a => a.OnConfirm, () =>
            {
                confirm = true;
                return Task.CompletedTask;
            });
        });

        var cell = cut.Find(".sidebar-item span.cell");
        cut.InvokeAsync(() => cell.Click());
        Assert.True(confirm);

        confirm = false;
        cut.Render(pb =>
        {
            pb.Add(a => a.ShowFooter, true);
            pb.Add(a => a.AutoClose, false);
        });
        cut.InvokeAsync(() => cell.Click());
        Assert.True(confirm);

        // 不显示 Footer AutoClose 参数不起作用自动关闭
        confirm = false;
        cut.Render(pb =>
        {
            pb.Add(a => a.ShowFooter, false);
            pb.Add(a => a.AutoClose, false);
        });
        cut.InvokeAsync(() => cell.Click());
        Assert.True(confirm);
    }

    [Fact]
    public void FormatValueAsString_DateTime_Ok()
    {
        // 设置为 最小值或者 null 时 当 AutoToday 为 true 时自动设置为当前时间
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.AutoToday, true);
            pb.Add(a => a.Value, DateTime.MinValue);
        });
        var input = cut.Find(".datetime-picker-input");
        Assert.Equal($"{DateTime.Today:yyyy-MM-dd}", input.GetAttribute("value"));
        Assert.Equal(DateTime.Today, cut.Instance.Value);

        // 设置为 禁用日期时 UI 显示为空字符串
        cut.Render(pb =>
        {
            pb.Add(a => a.DisplayDisabledDayAsEmpty, true);
            pb.Add(a => a.Value, DateTime.MinValue);
            pb.Add(a => a.AutoToday, true);
            pb.Add(a => a.OnGetDisabledDaysCallback, async (start, end) =>
            {
                await Task.Delay(0);
                var ret = new List<DateTime>() { DateTime.Today };
                return ret;
            });
        });
        Assert.Equal("", input.GetAttribute("value"));

        // 禁用 AutoToday 显示 0001-01-01
        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTime.MinValue);
            pb.Add(a => a.AutoToday, false);
        });
        Assert.Equal("0001-01-01", input.GetAttribute("value"));

        // 更改值为未禁用日期
        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTime.Today.AddDays(-1));
        });
        Assert.Equal($"{DateTime.Today.AddDays(-1):yyyy-MM-dd}", input.GetAttribute("value"));
    }

    [Fact]
    public void FormatValueAsString_DateTimeOffset_Ok()
    {
        // 设置为 最小值或者 null 时 当 AutoToday 为 true 时自动设置为当前时间
        var cut = Context.Render<DateTimePicker<DateTimeOffset?>>(pb =>
        {
            pb.Add(a => a.DisplayDisabledDayAsEmpty, true);
            pb.Add(a => a.Value, null);
            pb.Add(a => a.AutoToday, false);
            pb.Add(a => a.OnGetDisabledDaysCallback, async (start, end) =>
            {
                await Task.Delay(0);
                var ret = new List<DateTime>() { DateTime.Today };
                return ret;
            });
        });
        var input = cut.Find(".datetime-picker-input");
        Assert.Equal("", input.GetAttribute("value"));
        Assert.Null(cut.Instance.Value);

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTimeOffset.Now);
            pb.Add(a => a.DisplayDisabledDayAsEmpty, false);
        });
        Assert.Equal($"{DateTimeOffset.Now:yyyy-MM-dd}", input.GetAttribute("value"));

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTimeOffset.MinValue);
            pb.Add(a => a.AutoToday, false);
            pb.Add(a => a.DisplayMinValueAsEmpty, false);
            pb.Add(a => a.DisplayDisabledDayAsEmpty, false);
        });
        Assert.Equal($"0001-01-01", input.GetAttribute("value"));

        cut.Render(pb =>
        {
            pb.Add(a => a.Value, DateTimeOffset.MinValue);
            pb.Add(a => a.AutoToday, true);
            pb.Add(a => a.DisplayDisabledDayAsEmpty, true);
        });
        Assert.Equal($"0001-01-01", input.GetAttribute("value"));
    }

    [Fact]
    public async Task IsEditable_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime>>(pb =>
        {
            pb.Add(a => a.IsEditable, true);
            pb.Add(a => a.ViewMode, DatePickerViewMode.Date);
            pb.Add(a => a.DateFormat, "MM/dd/yyyy");
        });
        var input = cut.Find(".datetime-picker-input");
        Assert.False(input.HasAttribute("readonly"));

        // input value
        await cut.InvokeAsync(() =>
        {
            input.Change("02/15/2024");
        });
        Assert.Equal("02/15/2024", cut.Instance.Value.ToString("MM/dd/yyyy"));

        // error input value
        await cut.InvokeAsync(() =>
        {
            input.Change("test");
        });
        Assert.Equal("02/15/2024", cut.Instance.Value.ToString("MM/dd/yyyy"));

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            pb.Add(a => a.DateTimeFormat, "MM/dd/yyyy HH:mm:ss");
        });
        await cut.InvokeAsync(() =>
        {
            input.Change("02/15/2024 01:00:00");
        });
        Assert.Equal("02/15/2024 01:00:00", cut.Instance.Value.ToString("MM/dd/yyyy HH:mm:ss"));
    }

    [Fact]
    public void MinValueToEmpty_Ok()
    {
        var cut = Context.Render<DateTimePicker<DateTime?>>(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.Date);
            pb.Add(a => a.Value, DateTime.MinValue);
            pb.Add(a => a.DisplayMinValueAsEmpty, true);
        });
        Assert.Null(cut.Instance.Value);

        cut.Render(pb =>
        {
            pb.Add(a => a.ViewMode, DatePickerViewMode.DateTime);
            pb.Add(a => a.Value, DateTime.MinValue);
            pb.Add(a => a.DisplayMinValueAsEmpty, true);
        });
        Assert.Null(cut.Instance.Value);
    }

    [Fact]
    public async Task OnGetDisabledDaysCallback_Ok()
    {
        var fetched = false;
        var dtm = new DateTime(2024, 9, 25);
        // 禁用当天
        var cut = Context.Render<DateTimePicker<DateTime?>>(pb =>
        {
            pb.Add(a => a.OnGetDisabledDaysCallback, async (start, end) =>
            {
                fetched = true;
                await Task.Yield();
                return [dtm];
            });
            pb.Add(a => a.Value, dtm);
        });

        // 组件值为 null
        Assert.True(fetched);
        Assert.Equal(dtm, cut.Instance.Value);

        // 设置组件值不为当前天
        // 相同月数据已缓存不会触发回调
        fetched = false;
        cut.Render(pb =>
        {
            pb.Add(a => a.Value, dtm.AddDays(1));
        });
        Assert.False(fetched);
        Assert.Equal(dtm.AddDays(1), cut.Instance.Value);

        // 禁用缓存
        // 每次组件渲染都会触发回调
        cut.Render(pb =>
        {
            pb.Add(a => a.EnableDisabledDaysCache, false);
        });

        // 获得所有按钮
        var buttons = cut.FindAll(".picker-panel-header button");

        // 下一月
        await cut.InvokeAsync(() => buttons[2].Click());
        Assert.True(fetched);

        // 上一月
        // 数据已缓存不会触发回调
        fetched = false;
        await cut.InvokeAsync(() => buttons[1].Click());
        Assert.False(fetched);

        // 上一年
        await cut.InvokeAsync(() => buttons[0].Click());
        Assert.True(fetched);

        // 下一年
        // 数据已缓存不会触发回调
        fetched = false;
        await cut.InvokeAsync(() => buttons[3].Click());
        Assert.False(fetched);

        // 调用清除缓存方法
        cut.Instance.ClearDisabledDays();
    }

    [Fact]
    public async Task OnBlurAsync_Ok()
    {
        var blur = false;
        var cut = Context.Render<DateTimePicker<DateTime>>(builder =>
        {
            builder.Add(a => a.OnBlurAsync, v =>
            {
                blur = true;
                return Task.CompletedTask;
            });
        });
        var input = cut.Find("input");
        await cut.InvokeAsync(() => { input.Blur(); });
        Assert.True(blur);
    }

    class MockDateTimePicker : DatePickerBody
    {
        public static bool GetSafeYearDateTime_Ok()
        {
            var dtm = GetSafeYearDateTime(DateTime.MaxValue, 1);
            return dtm == DateTime.MaxValue.Date;
        }
    }
}
