# test add/remove notes
# Derek Fujimoto
# March 2026

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

def database_path(client):
    """Absolute path to the current state's notes database

    Built from __file__, not the working directory: an "os.path.exists('../data/...')" check
    silently passes from anywhere else, which would make every "the file is gone" assertion
    vacuous.
    """
    current_state = client.odb_get("/Shifts/Variables/stateid")
    path = os.path.dirname(os.path.abspath(__file__))
    return os.path.join(path, '..', 'data', str(current_state), 'notes_database.csv')

def get_database(client):
    return pd.read_csv(database_path(client))

# set note - check database
def test_note_set(page, client, users_added):

    # write a note
    write_note(page, 1, 'schedule', 'test note 1')

    # check that the note is in the database
    @try_again
    def check():
        df = get_database(client)

        assert len(df) == 1, \
            f"Expected 1 row in notes database after writing one note, got {len(df)}"
        assert df.loc[0,'note'] == 'test note 1', \
            f"Expected note text to be 'test note 1', got '{df.loc[0, 'note']}'"
    check()

    # write another note
    write_note(page, 2, 'schedule', 'test note 2')

    # check that the note is in the database
    @try_again
    def check2():
        df = get_database(client)

        assert len(df) == 2, \
            f"Expected 2 rows in notes database after writing two notes, got {len(df)}"
        assert df.loc[0,'note'] == 'test note 1', \
            f"Expected first note to be 'test note 1', got '{df.loc[0, 'note']}'"
        assert df.loc[1,'note'] == 'test note 2', \
            f"Expected second note to be 'test note 2', got '{df.loc[1, 'note']}'"
    check2()

# set then edit note
def test_note_setedit(page, client, users_added):

    # write a note
    write_note(page, 1, 'schedule', 'test note 1')

    # edit note
    write_note(page, 1, 'schedule', 'blah')

    # check that the note is in the database
    @try_again
    def check():
        df = get_database(client)

        assert len(df) == 1, \
            f"Expected 1 row in notes database after writing and editing a note, got {len(df)}"
        assert df.loc[0,'note'] == 'blah', \
            f"Expected edited note text to be 'blah', got '{df.loc[0, 'note']}'"
    check()

# remove note
def test_note_rm(page, client, users_added):

    # write two notes
    write_note(page, 1, 'schedule', 'test note 1')
    write_note(page, 2, 'schedule', 'test note 2')

    # remove the note
    delete_note(page, 1, 'schedule')

    # check that the note is removed from the database
    @try_again
    def check():
        df = get_database(client)

        assert len(df) == 1, \
            f"Expected 1 row remaining after deleting one of two notes, got {len(df)}"
        assert df.loc[0,'note'] == 'test note 2', \
            f"Expected remaining note to be 'test note 2', got '{df.loc[0, 'note']}'"
    check()

    # remove the second note
    delete_note(page, 2, 'schedule')

    # check that the database is deleted
    @try_again
    def check2():
        assert not os.path.exists(database_path(client)), \
            f"Expected notes_database.csv to be deleted after removing all notes, but file still exists at '{database_path(client)}'"
    check2()

# check display of note
def test_note_display_schedule(page, users_added):

    table = 'schedule'
    write_note(page, 1, table, 'test note 1')
    label = by_id(page, f'{table}_note_label_1')

    @try_again
    def check():
        assert label.inner_html() == 'test note 1', \
            f"Expected schedule note label to display 'test note 1', got '{label.inner_html()}'"
    check()

    delete_note(page, 1, table)

    @try_again
    def check2():
        assert label.inner_html() == '', \
            f"Expected schedule note label to be empty after deletion, got '{label.inner_html()}'"
    check2()

def test_note_display_avail(page, users_added):

    table = 'avail'
    write_note(page, 1, table, 'test note 1')
    label = by_id(page, f'{table}_note_label_1')
    @try_again
    def check():
        assert label.inner_html() == 'test note 1', \
            f"Expected avail note label to display 'test note 1', got '{label.inner_html()}'"
    check()

    delete_note(page, 1, table)

    @try_again
    def check2():
        assert label.inner_html() == '', \
            f"Expected avail note label to be empty after deletion, got '{label.inner_html()}'"
    check2()

# sync with avail table: schedule -> avail
def test_note_sync1(page, users_added):
    write_note(page, 1, 'schedule', 'test note 1')
    label = by_id(page, f'avail_note_label_1')

    @try_again
    def check():
        assert label.inner_html() == 'test note 1', \
            f"Expected avail note label to sync with schedule note 'test note 1', got '{label.inner_html()}'"
    check()

    delete_note(page, 1, 'schedule')
    @try_again
    def check2():
        assert label.inner_html() == '', \
            f"Expected avail note label to be empty after deleting schedule note, got '{label.inner_html()}'"
    check2()

# sync with avail table: avail -> schedule
def test_note_sync2(page, users_added):
    write_note(page, 1, 'avail', 'test note 1')
    label = by_id(page, f'schedule_note_label_1')

    @try_again
    def check():
        assert label.inner_html() == 'test note 1', \
            f"Expected schedule note label to sync with avail note 'test note 1', got '{label.inner_html()}'"
    check()

    delete_note(page, 1, 'avail')

    @try_again
    def check2():
        assert label.inner_html() == '', \
            f"Expected schedule note label to be empty after deleting avail note, got '{label.inner_html()}'"
    check2()

# check note persists on refresh
def test_note_load(page, users_added):

    for i in range(1, 4):
        write_note(page, i, 'schedule', f'test note {i}')

    page.reload()

    @try_again
    def check():
        for i in range(1, 4):
            label = by_id(page, f'schedule_note_label_{i}')
            assert label.inner_html() == f'test note {i}', \
                f"Expected schedule note {i} to persist as 'test note {i}' after refresh, got '{label.inner_html()}'"

            label = by_id(page, f'avail_note_label_{i}')
            assert label.inner_html() == f'test note {i}', \
                f"Expected avail note {i} to sync with schedule note 'test note {i}' after refresh, got '{label.inner_html()}'"
    check()

    # delete some notes
    delete_note(page, 2, 'schedule')
    page.reload()

    @try_again
    def check2():
        for i in range(1, 4):

            label1 = by_id(page, f'schedule_note_label_{i}')
            label2 = by_id(page, f'avail_note_label_{i}')
            if i != 2:
                assert label1.inner_html() == f'test note {i}', \
                    f"Expected schedule note {i} to persist as 'test note {i}' after deleting note 2 and refreshing, got '{label1.inner_html()}'"
                assert label2.inner_html() == f'test note {i}', \
                    f"Expected avail note {i} to sync with 'test note {i}' after deleting note 2 and refreshing, got '{label2.inner_html()}'"
            else:
                assert label1.inner_html() == '', \
                    f"Expected deleted schedule note 2 to be empty after refresh, got '{label1.inner_html()}'"
                assert label2.inner_html() == '', \
                    f"Expected avail note 2 to be empty after deleting schedule note 2 and refreshing, got '{label2.inner_html()}'"
    check2()

# check that the note input is hidden and the label is visible by default
def test_note_input_hidden_by_default(page, users_added):

    @try_again
    def check0():
        for table in ('schedule', 'avail'):
            for col in range(1, 4):
                note_input = by_id(page, f'{table}_note_input_{col}')
                note_label = by_id(page, f'{table}_note_label_{col}')

                assert note_input.evaluate('el => el.hidden') == True, \
                    f"Expected {table} note input at col {col} to be hidden by default, but it was visible"
                assert note_label.evaluate('el => el.hidden') == False, \
                    f"Expected {table} note label at col {col} to be visible by default, but it was hidden"
    check0()

# check that clicking a note cell shows the input and hides the label,
# and that moving away restores the original state
def test_note_input_shows_on_click(page, users_added):

    for table in ('schedule', 'avail'):
        col = 1
        cell = by_id(page, f'{table}_note_{col}')
        note_input = by_id(page, f'{table}_note_input_{col}')
        note_label = by_id(page, f'{table}_note_label_{col}')

        # click the cell
        cell.click()

        @try_again
        def check0():
            assert note_input.evaluate('el => el.hidden') == False, \
                f"Expected {table} note input at col {col} to be visible after clicking the cell, but it was hidden"
            assert note_label.evaluate('el => el.hidden') == True, \
                f"Expected {table} note label at col {col} to be hidden after clicking the cell, but it was visible"
        check0()

        # move focus away by triggering mouseleave
        note_input.hover()
        page.locator('h1').hover()
        time.sleep(0.05)

        @try_again
        def check1():
            assert note_input.evaluate('el => el.hidden') == True, \
                f"Expected {table} note input at col {col} to be hidden after moving focus away, but it was still visible"
            assert note_label.evaluate('el => el.hidden') == False, \
                f"Expected {table} note label at col {col} to be visible after moving focus away, but it was hidden"
        check1()

# check that moving the mouse off a note cell saves the note without pressing Enter
def test_note_mouseleave_saves(page, client, users_added):

    col = 1
    table = 'schedule'
    cell = by_id(page, f'{table}_note_{col}')
    note_input = by_id(page, f'{table}_note_input_{col}')

    # click and type a note without pressing Enter
    cell.click()
    note_input.press_sequentially('mouseleave note')

    # move away to trigger onmouseleave
    note_input.hover()
    page.locator('h1').hover()
    time.sleep(0.1)

    @try_again
    def check0():

        # check database was written
        df = get_database(client)

        assert len(df) == 1, \
            f"Expected 1 row in notes database after saving via mouseleave, got {len(df)}"
        assert df.loc[0, 'note'] == 'mouseleave note', \
            f"Expected note text to be 'mouseleave note' after saving via mouseleave, got '{df.loc[0, 'note']}'"

        # check the label also updated
        note_label = by_id(page, f'{table}_note_label_{col}')
        assert note_label.inner_html() == 'mouseleave note', \
            f"Expected {table} note label at col {col} to show 'mouseleave note' after mouseleave save, got '{note_label.inner_html()}'"
    check0()

# check that moving away without changing the note text does not write to the database
def test_note_no_save_if_unchanged(page, client, users_added):

    col = 1
    table = 'schedule'

    # record state id before any interaction
    state_before = client.odb_get("/Shifts/Variables/stateid")

    cell = by_id(page, f'{table}_note_{col}')

    # click and immediately move away without typing anything
    cell.click()
    cell.hover()
    page.locator('h1').hover()
    time.sleep(0.1)

    @try_again
    def check0():

        # state id should be unchanged: no write occurred
        state_after = client.odb_get("/Shifts/Variables/stateid")
        assert state_after == state_before, \
            f"Expected state ID to be unchanged after clicking and leaving a note cell without editing, " \
            f"but state changed from {state_before} to {state_after}"

        # database file should not exist
        assert not os.path.exists(database_path(client)), \
            f"Expected no notes_database.csv to be created when no note text was changed, " \
            f"but file exists at '{database_path(client)}'"

    check0()

# notes containing the character used as the wire-format delimiter must survive a round
# trip: joining them with "_" used to split one note into several, putting every note
# after it on the wrong day
def test_note_underscore_roundtrip(page, client, users_added):

    write_note(page, 1, 'schedule', 'owl_shift note')
    write_note(page, 2, 'schedule', 'second note')

    @try_again
    def check():
        df = get_database(client).sort_values('time').reset_index(drop=True)

        assert len(df) == 2, \
            f"Expected 2 rows in notes database, got {len(df)}: {df['note'].tolist()}"
        assert df.loc[0, 'note'] == 'owl_shift note', \
            f"Expected the underscore to survive the round trip, got '{df.loc[0, 'note']}'"
    check()

    # reload so the labels are rebuilt from the wire format rather than left as typed:
    # that is the only path a delimiter collision shows up on
    page.reload()

    @try_again
    def check_labels():
        assert by_id(page, 'schedule_note_label_1').inner_html() == 'owl_shift note', \
            f"Expected first note label to read 'owl_shift note', got " \
            f"'{by_id(page, 'schedule_note_label_1').inner_html()}'"
        assert by_id(page, 'schedule_note_label_2').inner_html() == 'second note', \
            f"Expected second note label to read 'second note', got " \
            f"'{by_id(page, 'schedule_note_label_2').inner_html()}'"
    check_labels()

# the light blue a note cell resets to on mouseleave, as the browser reports it
NOTE_CELL_COLOR = 'rgb(180, 231, 255)'

def cell_color(locator):
    """Rendered background colour of an element, as an "rgb(r, g, b)" string"""
    return locator.evaluate('el => getComputedStyle(el).backgroundColor')

# hovering darkens a note cell and leaving restores it. an unpaired mouseenter - the browser
# re-dispatches boundary events when pressing Enter hides the input under the cursor - used
# to be darkened a second time and latched in as the colour to restore, leaving the cell dark
# for as long as the calendar stood
def test_note_cell_color_survives_unpaired_enter(page, users_added):

    for table in ('schedule', 'avail'):
        cell = by_id(page, f'{table}_note_1')

        assert cell_color(cell) == NOTE_CELL_COLOR, \
            f"Expected the {table} note cell to start at {NOTE_CELL_COLOR}, got {cell_color(cell)}"

        # two enters for one leave: the pairing the browser is not guaranteed to keep
        cell.dispatch_event('mouseenter')
        darkened = cell_color(cell)
        cell.dispatch_event('mouseenter')
        cell.dispatch_event('mouseleave')

        assert darkened != NOTE_CELL_COLOR, \
            f"Expected the {table} note cell to darken on mouseenter, but it stayed {darkened}"

        @try_again
        def check():
            assert cell_color(cell) == NOTE_CELL_COLOR, \
                f"Expected the {table} note cell to reset to {NOTE_CELL_COLOR} after mouseleave, " \
                f"got {cell_color(cell)}"
        check()

# writing a note and moving the mouse off leaves the cell its normal colour
def test_note_cell_color_after_write(page, users_added):

    cell = by_id(page, 'schedule_note_1')

    write_note(page, 1, 'schedule', 'colour check')

    # move the mouse off the cell, which is what resets the colour
    page.locator('h1').hover()

    @try_again
    def check():
        assert cell_color(cell) == NOTE_CELL_COLOR, \
            f"Expected the note cell to be {NOTE_CELL_COLOR} after writing a note and moving " \
            f"away, got {cell_color(cell)}"
    check()
