# test availability calendar
# Derek Fujimoto
# Mar 2026

from fixtures import *
import time, os
import pandas as pd

def get_database(client):
    current_state = client.odb_get("/Shifts/Variables/stateid")
    path = os.path.dirname(os.path.abspath(__file__))
    path = os.path.join(path, '..', 'data', str(current_state), 'avail_database.csv')
    return pd.read_csv(path)

# set avail and check database
def test_set(page, client, users_added):

    # set avail
    select_user(page, 'test0')
    cell = by_id(page, 'availcell_1_1')
    cell.click()

    @try_again
    def check():
        df = get_database(client)

        assert len(df) == 1, \
            f"Expected 1 row in database after single cell click, got {len(df)}"

        # check row
        assert df.time[0] % 1000 == 1, \
            f"Expected time column index to be 1 (time % 1000 == 1), got {df.time[0] % 1000}"

        # check cols
        assert df.columns[1] == 'test0', \
            f"Expected second column to be 'test0', got '{df.columns[1]}'"

        # check cell
        assert df.loc[0, 'test0'] == True, \
            f"Expected cell (0, 'test0') to be True, got {df.loc[0, 'test0']}"
    check()

# set avail via multiple clicks and check database
def test_set_mult(page, client, users_added):

    # set avail
    select_user(page, 'test0')
    for row in [1,2]:
        for col in [1,6,2]:
            cell = by_id(page, f'availcell_{row}_{col}')
            cell.click()

    @try_again
    def check():
        df = get_database(client)

        assert len(df) == 6, \
            f"Expected 6 rows in database after 6 cell clicks, got {len(df)}"

        # check row
        for i in range(3):
            assert df.time[i] % 1000 == 1, \
                f"Expected row {i} to have time index 1 (time % 1000 == 1), got {df.time[i] % 1000}"
            assert df.time[i+3] % 1000 == 2, \
                f"Expected row {i+3} to have time index 2 (time % 1000 == 2), got {df.time[i+3] % 1000}"

        # check cols
        assert len(df.columns) == 2, \
            f"Expected 2 columns ('time', 'test0'), got {len(df.columns)}: {list(df.columns)}"
        assert df.columns[0] == 'time', \
            f"Expected first column to be 'time', got '{df.columns[0]}'"
        assert df.columns[1] == 'test0', \
            f"Expected second column to be 'test0', got '{df.columns[1]}'"

        # check cell
        assert all(df.loc[:, 'test0']), \
            f"Expected all cells in 'test0' column to be True, got {df.loc[:, 'test0'].tolist()}"
    check()

# set avail via click and drag and check database
def test_set_drag(page, client, users_added):

    select_user(page, 'test0')

    # drag path (row, col)
    path = ((1,1),
            (1,2),
            (1,3),
            (1,4),
            (2,4),
            (2,3),
            (2,2),
            (3,2),
            (3,3),
            )
    click_and_drag(page, path)

    @try_again
    def check():
        df = get_database(client)

        assert len(df.columns) == 2, \
            f"Expected 2 columns ('time', 'test0'), got {len(df.columns)}: {list(df.columns)}"
        assert df.columns[0] == 'time', \
            f"Expected first column to be 'time', got '{df.columns[0]}'"
        assert df.columns[1] == 'test0', \
            f"Expected second column to be 'test0', got '{df.columns[1]}'"

        assert all(df.loc[:3, 'time'] % 1000 == 1), \
            f"Expected rows 0-3 to have time index 1, got {(df.loc[:3, 'time'] % 1000).tolist()}"
        assert all(df.loc[4:6, 'time'] % 1000 == 2), \
            f"Expected rows 4-6 to have time index 2, got {(df.loc[4:6, 'time'] % 1000).tolist()}"
        assert all(df.loc[7:, 'time'] % 1000 == 3), \
        f"Expected rows 7+ to have time index 3, got {(df.loc[7:, 'time'] % 1000).tolist()}"
    check()

# set avail via click and drag and check database
# this path doubles back on itself, but should result in the same setting as
# the previous test
def test_set_drag2(page, client, users_added):

    select_user(page, 'test0')

    # drag path (row, col)
    path = ((1,1),
            (1,2),
            (1,3),
            (1,4),
            (2,4),
            (2,3),
            (2,2),
            (3,2),
            (3,3),
            (3,2),
            (2,2),
            (2,3),
            (2,4),
            )
    click_and_drag(page, path)

    @try_again
    def check():
        df = get_database(client)

        assert len(df.columns) == 2, \
            f"Expected 2 columns ('time', 'test0'), got {len(df.columns)}: {list(df.columns)}"
        assert df.columns[0] == 'time', \
            f"Expected first column to be 'time', got '{df.columns[0]}'"
        assert df.columns[1] == 'test0', \
            f"Expected second column to be 'test0', got '{df.columns[1]}'"

        assert all(df.loc[:3, 'time'] % 1000 == 1), \
            f"Expected rows 0-3 to have time index 1 after doubling-back drag, got {(df.loc[:3, 'time'] % 1000).tolist()}"
        assert all(df.loc[4:6, 'time'] % 1000 == 2), \
            f"Expected rows 4-6 to have time index 2 after doubling-back drag, got {(df.loc[4:6, 'time'] % 1000).tolist()}"
        assert all(df.loc[7:, 'time'] % 1000 == 3), \
            f"Expected rows 7+ to have time index 3 after doubling-back drag, got {(df.loc[7:, 'time'] % 1000).tolist()}"
    check()

# remove availability by clicking
def test_rm_click(page, client, users_added):

    # set
    select_user(page, 'test0')

    # drag path (row, col)
    path = ((1,1),
            (1,2),
            (1,3),
            )
    click_and_drag(page, path)

    @try_again
    def check1():
        df = get_database(client)

        assert len(df) == 3, \
            f"Expected 3 rows after dragging across 3 cells, got {len(df)}"
        assert all(df.time.diff().dropna() == 86400000), \
        f"Expected all consecutive time differences to be 86400000ms (1 day), got {df.time.diff().dropna().tolist()}"
    check1()

    # remove one
    cell = by_id(page, 'availcell_1_2')
    cell.click()

    @try_again
    def check2():
        df = get_database(client)

        assert len(df) == 2, \
            f"Expected 2 rows after removing 1 of 3 cells, got {len(df)}"
        assert df.loc[0, 'time'] % 1000 == 1, \
            f"Expected first remaining row to have time index 1, got {df.loc[0, 'time'] % 1000}"
        assert df.loc[1, 'time'] % 1000 == 1, \
            f"Expected second remaining row to have time index 1, got {df.loc[1, 'time'] % 1000}"
        assert all(df.time.diff().dropna() == 86400000*2), \
            f"Expected time gap of 86400000*2ms (2 days) after removing middle cell, got {df.time.diff().dropna().tolist()}"
    check2()

# remove availability by dragging
def test_rm_drag(page, client, users_added):

    # set
    select_user(page, 'test0')

    # drag path (row, col)
    path = ((1,1),
            (1,2),
            (1,3),
            (1,4),
            (1,5),
            )
    click_and_drag(page, path)

    # remove
    path = ((1,5),
            (1,4),
            (1,3),
            )
    click_and_drag(page, path)

    @try_again
    def check():
        df = get_database(client)
        assert len(df) == 2, \
            f"Expected 2 rows after setting 5 cells and removing 3, got {len(df)}"
        assert all(df.time.diff().dropna() == 86400000), \
        f"Expected consecutive time difference of 86400000ms (1 day) for remaining cells, got {df.time.diff().dropna().tolist()}"
    check()

# check availability numbers
def test_avail_numbers(page, users_added):

    select_user(page, 'test0')

    # drag path (row, col)
    path = ((1,1),
            (1,2),
            (1,3),
            (1,4),
            (1,5),
            )
    click_and_drag(page, path)

    # check
    @try_again
    def check():
        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                val = cell.inner_html()

                if row == 1 and col in (1,2,3,4,5):
                    assert val == '1', \
                        f"Expected count '1' for test0 at (row {row}, col {col}), got '{val}'"
                else:
                    assert val == '', \
                        f"Expected empty cell at (row {row}, col {col}), got '{val}'"
    check()

    # set a second person
    select_user(page, 'test1')

    # drag path (row, col)
    path = ((1,2),
            (1,3),
            (1,4),
            (1,5),
            )
    click_and_drag(page, path)

    # check
    @try_again
    def check2():
        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                val = cell.inner_html()

                if row == 1 and col == 1:
                    assert val == '1', \
                        f"Expected count '1' (test0 only) at (row {row}, col {col}), got '{val}'"
                elif row == 1 and col < 6:
                    assert val == '2', \
                        f"Expected count '2' (test0 + test1) at (row {row}, col {col}), got '{val}'"
                else:
                    assert val == '', \
                        f"Expected empty cell at (row {row}, col {col}), got '{val}'"
    check2()

    # set a third person
    select_user(page, 'test2')

    # drag path (row, col)
    path = ((1,3),
            (1,4),
            (1,5),
            )
    click_and_drag(page, path)

    # check
    @try_again
    def check3():
        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                val = cell.inner_html()

                if row == 1 and col == 1:
                    assert val == '1', \
                        f"Expected count '1' (test0 only) at (row {row}, col {col}), got '{val}'"
                elif row == 1 and col == 2:
                    assert val == '2', \
                        f"Expected count '2' (test0 + test1) at (row {row}, col {col}), got '{val}'"
                elif row == 1 and col < 6:
                    assert val == '3', \
                        f"Expected count '3' (test0 + test1 + test2) at (row {row}, col {col}), got '{val}'"
                else:
                    assert val == '', \
                        f"Expected empty cell at (row {row}, col {col}), got '{val}'"
    check3()

    # remove third person
    # drag path (row, col)
    path = ((1,3),
            (1,4),
            (1,5),
            )
    click_and_drag(page, path)

    # check
    @try_again
    def check4():
        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                val = cell.inner_html()

                if row == 1 and col == 1:
                    assert val == '1', \
                        f"Expected count '1' (test0 only) after removing test2 at (row {row}, col {col}), got '{val}'"
                elif row == 1 and col < 6:
                    assert val == '2', \
                        f"Expected count '2' (test0 + test1) after removing test2 at (row {row}, col {col}), got '{val}'"
                else:
                    assert val == '', \
                        f"Expected empty cell at (row {row}, col {col}), got '{val}'"
    check4()

# check colors
def test_avail_colors(page, users_added):
    select_user(page, 'test0')

    # drag path (row, col)
    path = ((1,1),
            (1,2),
            (1,3),
            (1,4),
            (1,5),
            )
    click_and_drag(page, path)

    # check
    @try_again
    def check():
        target_color  = page.evaluate('COLOR_AVAILBLE')
        target_color = target_color.lower()
        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                color = conftest.rgb2hex(css_value(cell, 'background-color'))

                if row == 1 and col < 6:
                    assert color == target_color, \
                        f"Expected selected cell at (row {row}, col {col}) to have color '{target_color}' (COLOR_AVAILBLE), got '{color}'"
                else:
                    assert color == '#000000', \
                        f"Expected unselected cell at (row {row}, col {col}) to have color '#000000', got '{color}'"
    check()

# check that we can switch back and forth between users
def test_avail_switch_user(page, users_added):

    # set user 1
    select_user(page, 'test0')

    # drag path (row, col)
    path = ((1,1),
            (1,2),
            (1,3),
            )
    click_and_drag(page, path)

    # check
    target_color  = page.evaluate('COLOR_AVAILBLE')
    target_color = target_color.lower()

    @try_again
    def check_test0():
        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                color = conftest.rgb2hex(css_value(cell, 'background-color'))

                if row == 1 and col in (1, 2, 3):
                    assert color == target_color, \
                        f"Expected test0's selected cell at (row {row}, col {col}) to show color '{target_color}', got '{color}'"
                else:
                    assert color in ('#000000', '#d6eeee'), \
                        f"Expected unselected cell at (row {row}, col {col}) to be '#000000' or '#d6eeee', got '{color}'"
    check_test0()

    # set user2
    select_user(page, 'test1')

    # drag path (row, col)
    path = ((2,4),
            (2,5),
            (2,6),
            )
    click_and_drag(page, path)

    # check
    @try_again
    def check_test1():
        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                color = conftest.rgb2hex(css_value(cell, 'background-color'))

                if row == 2 and col in (4, 5, 6):
                    assert color == target_color, \
                        f"Expected test1's selected cell at (row {row}, col {col}) to show color '{target_color}', got '{color}'"
                else:
                    assert color in ('#000000', '#d6eeee'), \
                        f"Expected unselected cell at (row {row}, col {col}) to be '#000000' or '#d6eeee', got '{color}'"
    check_test1()

    # switch back to test0
    select_user(page, 'test0')
    check_test0()

    # switch back again
    select_user(page, 'test1')
    check_test1()

# check that set avail are upated into the GlobalAvail object
def test_avail_globalavail_set(page, users_added):

    select_user(page, 'test0')

    # drag path (row, col)
    path = ((1,1),
            (1,2),
            (2,3),
            )
    click_and_drag(page, path)

    @try_again
    def check():
        gavail  = page.evaluate('GlobalAvail')

        keys = list(gavail.keys())
        assert len(keys) == 1, \
            f"Expected 1 key in GlobalAvail after setting test0, got {len(keys)}: {keys}"
        assert keys[0] == 'test0', \
            f"Expected GlobalAvail key to be 'test0', got '{keys[0]}'"
        assert len(gavail['test0']) == 3, \
            f"Expected 3 entries for test0 in GlobalAvail, got {len(gavail['test0'])}"
        assert gavail['test0'][0] % 1000 == 1, \
            f"Expected test0's first entry to have time index 1, got {gavail['test0'][0] % 1000}"
        assert gavail['test0'][1] % 1000 == 1, \
            f"Expected test0's second entry to have time index 1, got {gavail['test0'][1] % 1000}"
        assert gavail['test0'][2] % 1000 == 2, \
            f"Expected test0's third entry to have time index 2, got {gavail['test0'][2] % 1000}"
    check()

    # remove a cell
    cell = by_id(page, f'availcell_1_1')
    cell.click()

    @try_again
    def check2():
        gavail  = page.evaluate('GlobalAvail')

        keys = list(gavail.keys())
        assert len(keys) == 1, \
            f"Expected 1 key in GlobalAvail after removing a cell, got {len(keys)}: {keys}"
        assert keys[0] == 'test0', \
            f"Expected GlobalAvail key to still be 'test0' after removal, got '{keys[0]}'"
        assert len(gavail['test0']) == 2, \
            f"Expected 2 entries for test0 after removing 1 cell, got {len(gavail['test0'])}"
        assert gavail['test0'][0] % 1000 == 1, \
            f"Expected test0's first remaining entry to have time index 1, got {gavail['test0'][0] % 1000}"
        assert gavail['test0'][1] % 1000 == 2, \
            f"Expected test0's second remaining entry to have time index 2, got {gavail['test0'][1] % 1000}"
    check2()

    # set another user

    select_user(page, 'test1')

    # drag path (row, col)
    path = ((2,1),
            (2,2),
            (2,3),
            (2,4),
            )
    click_and_drag(page, path)

    @try_again
    def check3():
        gavail  = page.evaluate('GlobalAvail')

        keys = list(gavail.keys())
        assert len(keys) == 2, \
            f"Expected 2 keys in GlobalAvail after setting test1, got {len(keys)}: {keys}"
        assert keys[0] == 'test0', \
            f"Expected first GlobalAvail key to be 'test0', got '{keys[0]}'"
        assert keys[1] == 'test1', \
            f"Expected second GlobalAvail key to be 'test1', got '{keys[1]}'"
        assert len(gavail['test0']) == 2, \
            f"Expected test0 to still have 2 entries after adding test1, got {len(gavail['test0'])}"
        assert len(gavail['test1']) == 4, \
            f"Expected test1 to have 4 entries, got {len(gavail['test1'])}"
        assert gavail['test0'][0] % 1000 == 1, \
            f"Expected test0's first entry to have time index 1, got {gavail['test0'][0] % 1000}"
        assert gavail['test0'][1] % 1000 == 2, \
            f"Expected test0's second entry to have time index 2, got {gavail['test0'][1] % 1000}"
        assert gavail['test1'][0] % 1000 == 2, \
            f"Expected test1's first entry to have time index 2, got {gavail['test1'][0] % 1000}"
        assert gavail['test1'][1] % 1000 == 2, \
            f"Expected test1's second entry to have time index 2, got {gavail['test1'][1] % 1000}"
        assert gavail['test1'][2] % 1000 == 2, \
            f"Expected test1's third entry to have time index 2, got {gavail['test1'][2] % 1000}"
        assert gavail['test1'][3] % 1000 == 2, \
            f"Expected test1's fourth entry to have time index 2, got {gavail['test1'][3] % 1000}"
    check3()

# check that global param are updated properly after refresh
def test_avail_globalavail_refresh(page, users_added):
    select_user(page, 'test0')

    # drag path (row, col)
    path = ((1,1),
            (1,2),
            (2,3),
            )
    click_and_drag(page, path)

    page.reload()

    select_user(page, 'test0')

    @try_again
    def check():
        gavail  = page.evaluate('GlobalAvail')

        keys = list(gavail.keys())
        assert len(keys) == 1, \
            f"Expected 1 key in GlobalAvail after refresh, got {len(keys)}: {keys}"
        assert keys[0] == 'test0', \
            f"Expected GlobalAvail key to be 'test0' after refresh, got '{keys[0]}'"
        assert len(gavail['test0']) == 3, \
            f"Expected test0 to have 3 entries after refresh (state should be persisted), got {len(gavail['test0'])}"
        assert gavail['test0'][0] % 1000 == 1, \
            f"Expected test0's first entry to have time index 1 after refresh, got {gavail['test0'][0] % 1000}"
        assert gavail['test0'][1] % 1000 == 1, \
            f"Expected test0's second entry to have time index 1 after refresh, got {gavail['test0'][1] % 1000}"
        assert gavail['test0'][2] % 1000 == 2, \
            f"Expected test0's third entry to have time index 2 after refresh, got {gavail['test0'][2] % 1000}"
    check()

# check that updates are correct after refresh
def test_avail_refresh(page, users_added):

    # set some stuff from two users
    select_user(page, 'test0')
    path = ((1,1),
            (1,2),
            (2,3))
    click_and_drag(page, path)

    select_user(page, 'test1')
    path = ((2,1),
            (2,2),
            (3,3))
    click_and_drag(page, path)

    # get target color before refresh
    target_color  = page.evaluate('COLOR_AVAILBLE')
    target_color = target_color.lower()

    page.reload()

    @try_again
    def check():
        select_user(page, 'test0')

        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                color = conftest.rgb2hex(css_value(cell, 'background-color'))

                if (row == 1 and col in (1, 2)) or (row == 2 and col == 3):
                    assert color == target_color, \
                        f"Expected test0's selected cell at (row {row}, col {col}) to have color '{target_color}' (COLOR_AVAILBLE) after refresh, got '{color}'"
                else:
                    assert color in ('#000000', '#d6eeee'), \
                        f"Expected unselected cell at (row {row}, col {col}) to be '#000000' or '#d6eeee' when viewing test0 after refresh, got '{color}'"

    @try_again
    def check2():
        select_user(page, 'test1')

        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                color = conftest.rgb2hex(css_value(cell, 'background-color'))

                if (row == 2 and col in (1, 2)) or (row == 3 and col == 3):
                    assert color == target_color, \
                        f"Expected test1's selected cell at (row {row}, col {col}) to have color '{target_color}' (COLOR_AVAILBLE) after refresh, got '{color}'"
                else:
                    assert color in ('#000000', '#d6eeee'), \
                        f"Expected unselected cell at (row {row}, col {col}) to be '#000000' or '#d6eeee' when viewing test1 after refresh, got '{color}'"

    check()
    check2()

# check red colors on main page
def test_main_red(page, users_added):

    # set some stuff from a user
    select_user(page, 'test0')
    path = ((1,1),
            (1,2),
            (2,3))
    click_and_drag(page, path)

    # back to the main page
    select_user(page, '')

    # check
    @try_again
    def check():
        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                color = conftest.rgb2hex(css_value(cell, 'background-color'))

                if (row == 1 and col in (1, 2)) or (row == 2 and col == 3):
                    assert color in ('#000000', '#d6eeee', '#f0f0f0'), \
                        f"Expected available cell at (row {row}, col {col}) to show a neutral color ('#000000', '#d6eeee', or '#f0f0f0') on the main page, got '{color}'"
                else:
                    assert color not in ('#000000', '#d6eeee', '#f0f0f0'), \
                        f"Expected unavailable cell at (row {row}, col {col}) to show a non-neutral color (i.e. red) on the main page, got '{color}'"
    check()

    # check on refresh
    page.reload()
    check()


# check that a user cannot drag a cell unavailable if they are assigned to that shift
def test_avail_cannot_remove_if_assigned(page, client, users_added):

    # set availability for test0 on row 1, cols 1-3
    select_user(page, 'test0')
    path = ((1,1),
            (1,2),
            (1,3),
            )
    click_and_drag(page, path)

    # assign test0 to the shift at row 1, col 2
    cell = by_id(page, 'cell_1_2')
    cell.click()
    checkbox = by_id(page, 'test0_1_2_.')
    checkbox.click()
    time.sleep(0.05)

    # attempt to drag that cell unavailable
    path = ((1,2),
            (1,3),
            )
    click_and_drag(page, path)
    time.sleep(0.05)

    # check that a warning dialog appeared
    # midas appends a new dlgFrame per alert, so take the first to avoid strict mode
    @try_again
    def check():
        dialog = page.locator('.dlgFrame').first
        assert dialog.is_visible(), \
            f"Expected a warning dialog to appear when trying to remove availability from an assigned shift, but no dialog was shown"
    check()

    # dismiss the dialog
    ok_button = by_id(page, 'dlgMessageButton').first
    ok_button.click()
    time.sleep(0.05)

    # check that the cell is still marked available in the database
    @try_again
    def check2():
        df = get_database(client)

        assigned_times = df.loc[:, 'time']
        assigned_row_ids = [t % 1000 for t in assigned_times]
        assert 1 in assigned_row_ids, \
            f"Expected the assigned cell (row 1, col 2) to remain available in the database after failed removal attempt, " \
            f"but time index 1 was not found in {assigned_row_ids}"

        # check the cell color is still the available color
        target_color = page.evaluate('COLOR_AVAILBLE')
        target_color = target_color.lower()
        cell = by_id(page, 'availcell_1_2')
        color = conftest.rgb2hex(css_value(cell, 'background-color'))
        assert color == target_color, \
            f"Expected assigned cell at (row 1, col 2) to remain color '{target_color}' (COLOR_AVAILBLE) after failed removal, got '{color}'"
    check2()

# check that cell tooltip lists the correct available users
def test_avail_count_tooltip(page, users_added):

    # set availability: test0 on cols 1-3, test1 on cols 2-3, test2 on col 3 only
    select_user(page, 'test0')
    path = ((1,1),
            (1,2),
            (1,3),
            )
    click_and_drag(page, path)

    select_user(page, 'test1')
    path = ((1,2),
            (1,3),
            )
    click_and_drag(page, path)

    select_user(page, 'test2')
    path = ((1,3),
            (1,4),
            )
    click_and_drag(page, path)

    time.sleep(0.1)

    @try_again
    def check():
        # check tooltip on col 1: only test0
        cell = by_id(page, 'availcell_1_1')
        tooltip = cell.get_attribute('title')
        tooltip_names = sorted(tooltip.split('\n'))
        assert tooltip_names == ['test0'], \
            f"Expected tooltip at (row 1, col 1) to list only 'test0', got {tooltip_names}"

        # check tooltip on col 2: test0 and test1 (sorted)
        cell = by_id(page, 'availcell_1_2')
        tooltip = cell.get_attribute('title')
        tooltip_names = sorted(tooltip.split('\n'))
        assert tooltip_names == ['test0', 'test1'], \
            f"Expected tooltip at (row 1, col 2) to list 'test0' and 'test1', got {tooltip_names}"

        # check tooltip on col 3: test0, test1, and test2 (sorted)
        cell = by_id(page, 'availcell_1_3')
        tooltip = cell.get_attribute('title')
        tooltip_names = sorted(tooltip.split('\n'))
        assert tooltip_names == ['test0', 'test1', 'test2'], \
            f"Expected tooltip at (row 1, col 3) to list 'test0', 'test1', and 'test2', got {tooltip_names}"

        # check tooltip on col 4: only test2
        cell = by_id(page, 'availcell_1_4')
        tooltip = cell.get_attribute('title')
        tooltip_names = sorted(tooltip.split('\n'))
        assert tooltip_names == ['test2'], \
            f"Expected tooltip at (row 1, col 4) to list only 'test2', got {tooltip_names}"

        # check that a cell with no availability has no tooltip
        cell = by_id(page, 'availcell_1_5')
        tooltip = cell.get_attribute('title')
        assert tooltip == '' or tooltip is None, \
            f"Expected no tooltip at (row 1, col 5) with no availability, got '{tooltip}'"
    check()

# check that cells show count 0 and red coloring when no user is selected
def test_avail_count_zero_when_no_select(page, users_added):

    # set some availability so there are non-zero cells to compare against
    select_user(page, 'test0')
    path = ((1,1),
            (1,2),
            (1,3),
            )
    click_and_drag(page, path)

    select_user(page, 'test1')
    path = ((1,2),
            (1,3),
            )
    click_and_drag(page, path)

    time.sleep(0.1)

    # switch to no user selected (main page view)
    select_user(page, '')

    # check cells with no availability show count 0
    @try_again
    def check():
        for row in range(1, 4):
            for col in range(4, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                val = cell.inner_html()
                assert val == '0', \
                    f"Expected unavailable cell at (row {row}, col {col}) to show '0' when no user selected, got '{val}'"

        # check cells with availability show correct non-zero counts
        cell = by_id(page, 'availcell_1_1')
        val = cell.inner_html()
        assert val == '1', \
            f"Expected cell at (row 1, col 1) to show count '1' (test0 only) when no user selected, got '{val}'"

        cell = by_id(page, 'availcell_1_2')
        val = cell.inner_html()
        assert val == '2', \
            f"Expected cell at (row 1, col 2) to show count '2' (test0 + test1) when no user selected, got '{val}'"

        # check that zero-count cells are colored red (not neutral)
        neutral_colors = ('#000000', '#d6eeee', '#f0f0f0')
        for row in range(1, 4):
            for col in range(4, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                color = conftest.rgb2hex(css_value(cell, 'background-color'))
                assert color not in neutral_colors, \
                    f"Expected zero-availability cell at (row {row}, col {col}) to have a red (non-neutral) background when no user selected, got '{color}'"

    check()

# check that availability cells are redrawn correctly after the date window is changed
def test_avail_clear_on_date_change(page, client, users_added):

    # set availability for test0 in the current window
    select_user(page, 'test0')
    path = ((1,1),
            (1,2),
            (1,3),
            )
    click_and_drag(page, path)

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

    # confirm cells are colored before changing the date
    for col in (1, 2, 3):
        cell = by_id(page, f'availcell_1_{col}')
        color = conftest.rgb2hex(css_value(cell, 'background-color'))
        assert color == target_color, \
            f"Expected cell at (row 1, col {col}) to have color '{target_color}' before date change, got '{color}'"

    # advance the window by one week using the Next Week button
    next_week_button = by_id(page, 'tstart_nextweek')
    next_week_button.click()
    time.sleep(0.5)

    # check that no cells in the new window are colored with the available color,
    # since test0 has no availability set in the new date range
    @try_again
    def check():
        select_user(page, 'test0')

        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                color = conftest.rgb2hex(css_value(cell, 'background-color'))
                assert color != target_color, \
                    f"Expected cell at (row {row}, col {col}) to be cleared after advancing the date window (no availability in new range), got '{color}'"
    check()

    # return to the original week
    last_week_button = by_id(page, 'tstart_lastweek')
    last_week_button.click()
    time.sleep(0.5)

    # availability set in the original window should be restored
    @try_again
    def check2():
        select_user(page, 'test0')

        for row in range(1, 4):
            for col in range(1, 8):
                cell = by_id(page, f'availcell_{row}_{col}')
                color = conftest.rgb2hex(css_value(cell, 'background-color'))

                if row == 1 and col in (1, 2, 3):
                    assert color == target_color, \
                        f"Expected cell at (row {row}, col {col}) to show availability color '{target_color}' after returning to original week, got '{color}'"
                else:
                    assert color in ('#000000', '#d6eeee'), \
                        f"Expected unselected cell at (row {row}, col {col}) to be '#000000' or '#d6eeee' after returning to original week, got '{color}'"
    check2()

# dragging over a cell that is already available must not count that shifter twice: the
# drag state is latched from the first cell, so a drag starting on an unavailable cell
# re-sets every cell it crosses, including ones already set
def test_avail_no_double_count_on_redrag(page, users_added):

    # make (1,2) available on its own first
    select_user(page, 'test0')
    click_and_drag(page, ((1,2),))

    # now drag from an unavailable cell across it, so (1,2) gets set available again
    click_and_drag(page, ((1,1), (1,2), (1,3)))
    time.sleep(0.2)

    @try_again
    def check():
        # GlobalAvail is what the counts and tooltips are built from
        gavail = page.evaluate('GlobalAvail')
        times = gavail['test0']
        assert len(times) == len(set(times)), \
            f"Expected no duplicate timestamps in GlobalAvail for test0, got {times}"
        assert len(times) == 3, \
            f"Expected 3 available cells for test0, got {len(times)}: {times}"

        # the cell text is the availability count, which double-counts a duplicate
        count = by_id(page, 'availcell_1_2').inner_text()
        assert count == '1', \
            f"Expected availability count of 1 at (row 1, col 2) with one shifter available, got '{count}'"

        tooltip_names = by_id(page, 'availcell_1_2').get_attribute('title').split('\n')
        assert tooltip_names == ['test0'], \
            f"Expected tooltip at (row 1, col 2) to list test0 once, got {tooltip_names}"
    check()

# the availability count colouring scales between the least and most available cells.
# test_main_red only ever sets one user, which makes mincount == maxcount and sends every
# cell down the same branch, so the scaling itself needs a case with an actual range
def test_avail_count_colour_scale(page, users_added):

    # test0 available on cols 1-3, test1 on col 1 only: counts are 2, 1, 1
    select_user(page, 'test0')
    click_and_drag(page, ((1,1), (1,2), (1,3)))

    select_user(page, 'test1')
    click_and_drag(page, ((1,1),))

    # back to the overview, where the counts are coloured
    select_user(page, '')

    @try_again
    def check():
        most = conftest.rgb2hex(css_value(by_id(page, 'availcell_1_1'), 'background-color'))
        least = conftest.rgb2hex(css_value(by_id(page, 'availcell_1_2'), 'background-color'))

        assert most != least, \
            f"Expected the most-available cell and a less-available one to be coloured " \
            f"differently, but both were '{most}'"

        # the busiest cell sits at the top of the scale, which is the neutral grey
        assert most == '#f0f0f0', \
            f"Expected the most-available cell (2 of 2 shifters) to be neutral grey, got '{most}'"

        # a less-available cell is pushed towards red: high red, lower green and blue
        red, green, blue = (int(least[i:i+2], 16) for i in (1, 3, 5))
        assert red > green and red > blue, \
            f"Expected the less-available cell to be reddish, got '{least}'"
    check()
