# test date range controls and navigation
# Derek Fujimoto
# Mar 2026

from fixtures import *
import time

def date_value(locator):
    """Value of a date input, in milliseconds since the epoch"""
    return locator.evaluate('el => el.valueAsNumber')

def set_date(locator, value):
    """Set a date input to a value in milliseconds and trigger the redraw"""
    locator.evaluate("(el, v) => {el.valueAsNumber = v; el.dispatchEvent(new Event('change'));}",
                     value)

# check that the default radio button is 1wk and the stop date is 6 days after start
def test_date_range_radio_1wk(page, users_added):

    @try_again
    def check():
        radio = by_id(page, 'start_radio_6')
        assert radio.is_checked(), \
            f"Expected the '1wk' radio button to be selected by default, but it was not"

        start = date_value(by_id(page, 'start'))
        stop  = date_value(by_id(page, 'stop'))

        assert stop - start == 6 * 86400000, \
            f"Expected stop to be 6 days (6*86400000ms) after start by default, got {stop - start}ms"
    check()

# check that clicking the 2wk radio sets the window to 13 days and redraws
def test_date_range_radio_2wk(page, users_added):

    start_before = date_value(by_id(page, 'start'))

    by_id(page, 'start_radio_13').click()
    time.sleep(0.5)

    @try_again
    def check():
        start = date_value(by_id(page, 'start'))
        stop  = date_value(by_id(page, 'stop'))

        assert start == start_before, \
            f"Expected start date to be unchanged after clicking '2wk' radio, got {start} (was {start_before})"
        assert stop - start == 13 * 86400000, \
            f"Expected stop to be 13 days (13*86400000ms) after start after clicking '2wk', got {stop - start}ms"

        # calendar should have 14 columns (col 0 is labels, cols 1-14 are days)
        nheaders = page.locator('#schedule_body tr:first-child td').count()
        assert nheaders == 15, \
            f"Expected 15 header cells (label + 14 day columns) after switching to 2wk view, got {nheaders}"
    check()

# check that clicking the 3wk radio sets the window to 20 days and redraws
def test_date_range_radio_3wk(page, users_added):

    by_id(page, 'start_radio_20').click()
    time.sleep(0.5)

    start = date_value(by_id(page, 'start'))
    stop  = date_value(by_id(page, 'stop'))

    @try_again
    def check():
        assert stop - start == 20 * 86400000, \
            f"Expected stop to be 20 days (20*86400000ms) after start after clicking '3wk', got {stop - start}ms"

        nheaders = page.locator('#schedule_body tr:first-child td').count()
        assert nheaders == 22, \
            f"Expected 22 header cells (label + 21 day columns) after switching to 3wk view, got {nheaders}"
    check()

# check that clicking the 4wk radio sets the window to 27 days and redraws
def test_date_range_radio_4wk(page, users_added):

    by_id(page, 'start_radio_27').click()
    time.sleep(0.5)

    start = date_value(by_id(page, 'start'))
    stop  = date_value(by_id(page, 'stop'))

    @try_again
    def check():
        assert stop - start == 27 * 86400000, \
            f"Expected stop to be 27 days (27*86400000ms) after start after clicking '4wk', got {stop - start}ms"

        nheaders = page.locator('#schedule_body tr:first-child td').count()
        assert nheaders == 29, \
            f"Expected 29 header cells (label + 28 day columns) after switching to 4wk view, got {nheaders}"
    check()

# check that manually changing the start date deselects the radio and redraws
def test_date_range_manual_start(page, users_added):

    start_elem = by_id(page, 'start')
    stop_elem  = by_id(page, 'stop')

    stop_before = date_value(stop_elem)

    # advance start by 3 days
    set_date(start_elem, date_value(start_elem) + 3 * 86400000)
    time.sleep(0.5)

    @try_again
    def check():
        # no radio should be checked (duration is 3 days, which matches no preset)
        for radio_id in ('start_radio_6', 'start_radio_13', 'start_radio_20', 'start_radio_27'):
            radio = by_id(page, radio_id)
            assert not radio.is_checked(), \
                f"Expected radio '{radio_id}' to be deselected after manually changing start date, but it was still selected"

        # stop date should be unchanged
        stop_after = date_value(stop_elem)
        assert stop_after == stop_before, \
            f"Expected stop date to be unchanged after advancing start date by 3 days, got {stop_after} (was {stop_before})"
    check()

# check that manually changing the stop date redraws the calendar
def test_date_range_manual_stop(page, users_added):

    start_elem = by_id(page, 'start')
    stop_elem  = by_id(page, 'stop')

    start = date_value(start_elem)

    # set stop to exactly 9 days after start (not a preset)
    set_date(stop_elem, start + 9 * 86400000)
    time.sleep(0.5)

    @try_again
    def check():

        # calendar should have 11 header cells (label + 10 day columns)
        nheaders = page.locator('#schedule_body tr:first-child td').count()
        assert nheaders == 11, \
            f"Expected 11 header cells (label + 10 day columns) after setting stop to 9 days after start, got {nheaders}"
    check()

# check that setting stop before start auto-corrects stop to start + 6 days
def test_date_range_negative_duration(page, users_added):

    start_elem = by_id(page, 'start')
    stop_elem  = by_id(page, 'stop')

    start = date_value(start_elem)

    # set stop to a day before start
    set_date(stop_elem, start - 86400000)
    time.sleep(0.5)

    @try_again
    def check():

        # stop should have been auto-corrected to start + 6 days
        stop_corrected = date_value(stop_elem)
        assert stop_corrected == start + 6 * 86400000, \
            f"Expected stop to be auto-corrected to start + 6 days ({start + 6*86400000}) when set before start, got {stop_corrected}"

        # and the tables have to be redrawn for the corrected span, not the rejected one
        nheaders = page.locator('#schedule_body tr:first-child td').count()
        assert nheaders == 8, \
            f"Expected 8 cells (label + 7 days) after the span was corrected to one week, got {nheaders}"

        # the 1wk radio matches the corrected span, so it should end up checked
        assert by_id(page, 'start_radio_6').is_checked(), \
            "Expected the 1wk radio to be checked after the span was corrected to one week"
    check()

# check that a start date jumped far back drags stop along, keeping the 1wk window
def test_date_range_start_jump_preserves_window(page, users_added):

    start_elem = by_id(page, 'start')
    stop_elem  = by_id(page, 'stop')

    # jump start 60 days back, which would otherwise open a 66 day window
    start_jumped = date_value(start_elem) - 60 * 86400000
    set_date(start_elem, start_jumped)
    time.sleep(0.5)

    @try_again
    def check():

        start = date_value(start_elem)
        stop  = date_value(stop_elem)

        assert start == start_jumped, \
            f"Expected start to stay where it was set ({start_jumped}), got {start}"
        assert stop - start == 6 * 86400000, \
            f"Expected the 1wk window to be preserved after jumping start back 60 days, got {stop - start}ms"

        # the preserved span matches a preset, so its radio should end up checked
        assert by_id(page, 'start_radio_6').is_checked(), \
            "Expected the 1wk radio to be checked after the preserved span came back to one week"

        nheaders = page.locator('#schedule_body tr:first-child td').count()
        assert nheaders == 8, \
            f"Expected 8 cells (label + 7 days) after the window was preserved at one week, got {nheaders}"
    check()

# check that the preserved window is the one in use, not always one week
def test_date_range_start_jump_preserves_wide_window(page, users_added):

    start_elem = by_id(page, 'start')
    stop_elem  = by_id(page, 'stop')

    # widen the window to 3wk first
    by_id(page, 'start_radio_20').click()
    time.sleep(0.5)

    set_date(start_elem, date_value(start_elem) - 60 * 86400000)
    time.sleep(0.5)

    @try_again
    def check():

        start = date_value(start_elem)
        stop  = date_value(stop_elem)

        assert stop - start == 20 * 86400000, \
            f"Expected the 3wk window to be preserved after jumping start back 60 days, got {stop - start}ms"
    check()

# check that a start move inside the 4 week limit still stretches the window as before
def test_date_range_start_small_move_grows_window(page, users_added):

    start_elem = by_id(page, 'start')
    stop_elem  = by_id(page, 'stop')

    stop_before = date_value(stop_elem)

    # 7 days back on the 1wk default gives a 13 day window, which is under the limit
    set_date(start_elem, date_value(start_elem) - 7 * 86400000)
    time.sleep(0.5)

    @try_again
    def check():

        start = date_value(start_elem)
        stop  = date_value(stop_elem)

        assert stop == stop_before, \
            f"Expected stop to be unchanged after moving start back 7 days, got {stop} (was {stop_before})"
        assert stop - start == 13 * 86400000, \
            f"Expected the window to grow to 13 days after moving start back 7 days, got {stop - start}ms"
    check()

# check that the window limit applies to the start date only, not the stop date
def test_date_range_stop_jump_unrestricted(page, users_added):

    start_elem = by_id(page, 'start')
    stop_elem  = by_id(page, 'stop')

    start_before = date_value(start_elem)

    # push stop 60 days past start, well over the 4 week limit imposed on start
    set_date(stop_elem, start_before + 60 * 86400000)
    time.sleep(0.5)

    @try_again
    def check():

        start = date_value(start_elem)
        stop  = date_value(stop_elem)

        assert start == start_before, \
            f"Expected start to be unchanged after moving stop out 60 days, got {start} (was {start_before})"
        assert stop - start == 60 * 86400000, \
            f"Expected the 60 day window set on the stop date to be kept, got {stop - start}ms"
    check()

# check that clicking Last Week shifts start and stop back by 7 days
def test_nav_last_week(page, users_added):

    start_elem = by_id(page, 'start')
    stop_elem  = by_id(page, 'stop')

    start_before = date_value(start_elem)
    stop_before  = date_value(stop_elem)

    page.locator('xpath=//button[text()="Last Week"]').click()
    time.sleep(0.5)

    @try_again
    def check():

        start_after = date_value(start_elem)
        stop_after  = date_value(stop_elem)

        assert start_after == start_before - 7 * 86400000, \
            f"Expected start to shift back 7 days after clicking 'Last Week', got {start_after} (was {start_before})"
        assert stop_after == stop_before - 7 * 86400000, \
            f"Expected stop to shift back 7 days after clicking 'Last Week', got {stop_after} (was {stop_before})"
    check()

# check that clicking Next Week shifts start and stop forward by 7 days
def test_nav_next_week(page, users_added):

    start_elem = by_id(page, 'start')
    stop_elem  = by_id(page, 'stop')

    start_before = date_value(start_elem)
    stop_before  = date_value(stop_elem)

    page.locator('xpath=//button[text()="Next Week"]').click()
    time.sleep(0.5)

    @try_again
    def check():

        start_after = date_value(start_elem)
        stop_after  = date_value(stop_elem)

        assert start_after == start_before + 7 * 86400000, \
            f"Expected start to advance 7 days after clicking 'Next Week', got {start_after} (was {start_before})"
        assert stop_after == stop_before + 7 * 86400000, \
            f"Expected stop to advance 7 days after clicking 'Next Week', got {stop_after} (was {stop_before})"
    check()

# check that clicking Today resets start to today and maintains the window size
def test_nav_today(page, users_added):

    start_elem = by_id(page, 'start')
    stop_elem  = by_id(page, 'stop')

    # navigate away by two weeks
    page.locator('xpath=//button[text()="Next Week"]').click()
    time.sleep(0.3)
    page.locator('xpath=//button[text()="Next Week"]').click()
    time.sleep(0.3)

    duration_before = date_value(stop_elem) - date_value(start_elem)

    # click Today
    page.locator('xpath=//button[text()="Today"]').click()
    time.sleep(0.5)

    @try_again
    def check():
        start_after = date_value(start_elem)
        stop_after  = date_value(stop_elem)
        duration_after = stop_after - start_after

        # window size should be unchanged
        assert duration_after == duration_before, \
            f"Expected window duration to be preserved after clicking 'Today', got {duration_after}ms (was {duration_before}ms)"

        # start should be at or before today (may be snapped to Sunday depending on settings)
        today_epoch = page.evaluate('() => {let d = new ServerDate(); return d.getEpochDate(false);}')
        assert start_after == today_epoch, \
            f"Expected start date to be reset to today after clicking 'Today', got {start_after} (today epoch: {today_epoch})"
    check()

# check that the column corresponding to today is highlighted with COLOR_TODAY
def test_today_column_color(page, users_added):

    target_color = page.evaluate('COLOR_TODAY')
    target_color = target_color.lower()

    # find the today header cell by checking all header cells for the today color
    header_row = page.locator('#schedule_body tr:first-child td')
    avail_header_row = page.locator('#avail_body tr:first-child td')

    @try_again
    def check():

        today_cells = []
        for cell in header_row.all():
            color = conftest.rgb2hex(css_value(cell, 'background-color'))
            if color == target_color:
                today_cells.append(cell)

        assert len(today_cells) == 1, \
            f"Expected exactly 1 header cell to have COLOR_TODAY ({target_color}), found {len(today_cells)}"

        # the same column in the avail table header should also be highlighted
        avail_today_cells = []
        for cell in avail_header_row.all():
            color = conftest.rgb2hex(css_value(cell, 'background-color'))
            if color == target_color:
                avail_today_cells.append(cell)

        assert len(avail_today_cells) == 1, \
            f"Expected exactly 1 avail table header cell to have COLOR_TODAY ({target_color}), found {len(avail_today_cells)}"

    check()

# encode_timestamp and decode_timestamp have to be inverses at every server timezone. the
# page only ever runs west of utc here, where the bug this guards against is invisible, so
# the offset is overridden in the browser rather than on the host
def test_timestamp_roundtrip(page, users_added):

    for tz_hours in (-8, -3.5, 0, 2, 5.5, 13):

        decoded = page.evaluate("""(tz) => {
            let saved = server_timezone;
            server_timezone = tz*3600000;

            let out = [];
            for(let col=1; col<=7; col++){
                out.push(decode_timestamp(encode_timestamp(1, col)));
            }

            server_timezone = saved;
            return out;
        }""", tz_hours)

        for col, (shiftid, got_col) in enumerate(decoded, start=1):
            assert [shiftid, got_col] == [1, col], \
                f"Expected shiftid 1 column {col} to survive an encode/decode round trip " \
                f"at a server timezone of {tz_hours}h, got shiftid {shiftid} column {got_col}"
