# test statistics histograms
# Derek Fujimoto
# Mar 2026

from fixtures import *
from playwright.sync_api import expect
import time

def plot_data(page, plot_id, axis):
    """Data along one axis ('x' or 'y') of the histogram in this div, None if not drawn"""
    return page.evaluate(f'''() => {{
        let mpg = document.getElementById("{plot_id}").mpg;
        return mpg ? mpg.param.plot[0].{axis}Data : null;
    }}''')

def expect_plot_total(page, plot_id, axis, expected, note):
    """Wait for a histogram axis to total the expected value

    draw_assign_stats and draw_avail_stats both await an rpc before they touch the plot, so
    reading the data straight after a dropdown change races them and returns the previous
    filter's numbers.
    """
    @try_again
    def check():
        data = plot_data(page, plot_id, axis)
        assert data is not None, \
            f"Expected {plot_id} to have {axis}Data ({note}), but it was null"
        assert sum(data) == expected, \
            f"Expected {plot_id} {axis}Data to total {expected} ({note}), got {sum(data)}"
    check()

def expect_plot_labels(page, plot_id, expected, note):
    """Wait for a histogram's x labels to be exactly this set"""
    @try_again
    def check():
        labels = plot_data(page, plot_id, 'x')
        assert labels is not None, \
            f"Expected {plot_id} to have xData ({note}), but it was null"
        assert sorted(labels) == sorted(expected), \
            f"Expected {plot_id} x labels to be {sorted(expected)} ({note}), got {sorted(labels)}"
    check()

# check that the assignment histogram is rendered after making assignments
def test_stats_assign_histogram_rendered(page, users_added):

    # assign a few users to different cells
    assign(page, 0, 1, 1)
    assign(page, 0, 1, 2)
    assign(page, 1, 1, 3)
    time.sleep(0.2)

    # the mplot_assigned div should contain a canvas element (drawn by MPlotGraph)
    mplot = by_id(page, 'mplot_assigned')
    assert mplot.is_visible(), \
        f"Expected the assignment histogram div to be visible after making assignments, but it was not displayed"

    # counting does not wait, so give the plot a chance to be drawn first
    expect(mplot.locator('canvas').first).to_be_attached()

    ncanvases = mplot.locator('canvas').count()
    assert ncanvases > 0, \
        f"Expected at least one canvas element inside mplot_assigned after making assignments, got {ncanvases}"

    # check via JS that the plot object was created on the div
    has_plot = page.evaluate('document.getElementById("mplot_assigned").mpg !== undefined')
    assert has_plot, \
        f"Expected mplot_assigned.mpg to be set (MPlotGraph created) after making assignments, but it was undefined"

# check that the availability histogram is rendered after setting availability
def test_stats_avail_histogram_rendered(page, users_added):

    # set availability for a couple of users
    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.2)

    # the mplot_available div should contain a canvas element
    mplot = by_id(page, 'mplot_available')
    assert mplot.is_visible(), \
        f"Expected the availability histogram div to be visible after setting availability, but it was not displayed"

    # counting does not wait, so give the plot a chance to be drawn first
    expect(mplot.locator('canvas').first).to_be_attached()

    ncanvases = mplot.locator('canvas').count()
    assert ncanvases > 0, \
        f"Expected at least one canvas element inside mplot_available after setting availability, got {ncanvases}"

    has_plot = page.evaluate('document.getElementById("mplot_available").mpg !== undefined')
    assert has_plot, \
        f"Expected mplot_available.mpg to be set (MPlotGraph created) after setting availability, but it was undefined"

# check that filtering by shift in the stats dropdown updates the histogram data
def test_stats_filter_by_shift(page, users_added):

    # assign test0 to row 1 (shift 1), cols 1-3
    for col in range(1, 4):
        assign(page, 0, 1, col)

    # assign test1 to row 2 (shift 2), col 1 only
    assign(page, 1, 2, 1)

    time.sleep(0.2)

    # with no shift filter, both shifts are counted
    expect_plot_total(page, 'mplot_assigned', 'y', 4, 'no shift filter')

    # filter to shift 1 only using the stats shift select
    stats_shift_select = by_id(page, 'stats_shift_select')

    # get the option corresponding to shift row 1
    noptions = stats_shift_select.locator('option').count()
    assert noptions > 1, \
        f"Expected at least 2 options in the stats shift select (Any + at least one shift), got {noptions}"

    # select the first non-Any shift option
    stats_shift_select.select_option(index=1)
    time.sleep(0.2)

    # only the three row-1 assignments remain
    expect_plot_total(page, 'mplot_assigned', 'y', 3, 'filtered to shift 1')

# check that filtering by role in the stats dropdown updates the histogram data
def test_stats_filter_by_role(page, users_added):

    # assign test0 to col 1 with default role
    assign(page, 0, 1, 1)

    # assign test0 to col 2 with Cryo role
    select_role(page, 'Cryo')
    assign(page, 0, 1, 2, 'Cryo')

    # assign test1 to col 3 with Cryo role
    assign(page, 1, 1, 3, 'Cryo')

    time.sleep(0.2)

    # with no role filter, all three are counted
    expect_plot_total(page, 'mplot_assigned', 'y', 3, 'no role filter')

    # filter to 'Cryo' role
    by_id(page, 'stats_role_select').select_option('Cryo')
    time.sleep(0.2)

    expect_plot_total(page, 'mplot_assigned', 'y', 2, "filtered to the Cryo role")

# check that switching classify to Institution groups x-axis by affiliation
def test_stats_classify_by_institution(page, users_added):

    # set availability for test0 and test1 so the avail histogram has data
    select_user(page, 'test0')
    path = ((1,1),
            (1,2),
            (1,3),
            )
    click_and_drag(page, path)

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

    time.sleep(0.2)

    # default classification is by name
    expect_plot_labels(page, 'mplot_available', ['test0', 'test1'], 'Name mode')

    # switch to Institution classification
    classify_select = by_id(page, 'stats_classify_select')
    classify_select.select_option(label='Institution')
    time.sleep(0.2)

    # users_added gives test0 and test1 the affiliations uni0 and uni1. asserting the exact
    # set matters: "not a user name" also passes on the literal "undefined" the page
    # produces when the institution lookup misses, and on an empty list
    expect_plot_labels(page, 'mplot_available', ['uni0', 'uni1'], 'Institution mode')

    # switch back to Name and confirm labels revert
    classify_select.select_option(label='Name')
    time.sleep(0.2)

    expect_plot_labels(page, 'mplot_available', ['test0', 'test1'], 'switched back to Name')

# shift names are allowed to contain html, and the filter used to compare the option's
# innerHTML against the raw ODB string. an "&" re-serializes as "&amp;" on that round trip,
# so the name never matched and the filter silently fell back to "Any"
def test_stats_filter_shift_name_with_ampersand(page, client, users_added):

    names = client.odb_get('/Shifts/ShiftSetup/name')
    names[0] = 'Day & Night'
    client.odb_set('/Shifts/ShiftSetup/name', names)
    page.reload()
    expect(by_id(page, 'cell_1_1')).to_be_attached()

    # three assignments on the renamed shift, one on another
    for col in range(1, 4):
        assign(page, 0, 1, col)
    assign(page, 1, 2, 1)

    # assign() returns when the state id flips, which is before draw_assign_stats has even
    # been called, let alone finished its own rpc - so poll for the total rather than sleep
    expect_plot_total(page, 'mplot_assigned', 'y', 4, 'no shift filter')

    # filter to the renamed shift by its label
    by_id(page, 'stats_shift_select').select_option(label='Day & Night')

    expect_plot_total(page, 'mplot_assigned', 'y', 3,
                      "filtered to the shift named 'Day & Night'; 4 means the filter "
                      "silently fell back to Any")
