# Setup and tear down of environment for testing
# Derek Fujimoto
# Mar 2026

import pytest
import midas, midas.client
import subprocess
import os
import shutil 
import glob
import datetime

now = int(datetime.datetime.now().timestamp())
path = os.path.dirname(os.path.abspath(__file__))
odb_backup = f'{path}/backups/{now}_shiftschedule_testing.json'
data_backup = f'{path}/backups/{now}_data_bkup'
data = f'{path}/../data'

# set by backup(), read by restore()
backed_up_shifts = False

## HELPER FUNCTIONS ========================================================

def odbedit(command):
    """Run a single odbedit command and return its combined output

    odbedit exits 0 whether or not the command worked - a missing file or a missing key is
    reported in the text only - so every check in this module has to read the output. Using
    the return code silently passes on failures it looks like it is catching.

    Args:
        command (str): the odbedit command, ex: 'ls /Shifts'

    Returns:
        str: stdout and stderr, combined
    """
    result = subprocess.run(f'odbedit -c "{command}"', shell=True,
                            capture_output=True, text=True)
    return result.stdout + result.stderr

def odb_exists(odb_path):
    """Whether an ODB key exists

    Args:
        odb_path (str): ODB path, ex: '/Shifts'

    Returns:
        bool: True if odbedit could list the key
    """
    return 'not found' not in odbedit(f'ls {odb_path}')

def backup():
    global backed_up_shifts

    # remember whether the experiment had a /Shifts tree at all, so restore() knows whether
    # to insist on getting one back
    backed_up_shifts = odb_exists('/Shifts')

    # copy 
    os.makedirs(f'{path}/backups/', exist_ok=True)
    subprocess.run(f'odbedit -c "save {odb_backup}"', shell=True)
    print(f'ODB backed up as "{odb_backup}"')

    # a run aborted between clear() and restore() leaves no data dir, and so nothing to
    # back up: recreate it so the copy below has a source
    os.makedirs(data, exist_ok=True)
    shutil.copytree(data, data_backup)
    print(f'Data and history backed up as "{data_backup}"')

    # confirm these operations worked: files/paths exist and are not empty
    assert os.path.isfile(odb_backup), 'No odb backup file produced'
    assert os.path.getsize(odb_backup) > 0, 'odb backup file is empty'
    
    assert os.path.isdir(data_backup), 'no data backup dir produced'

    # data may legitimately be empty, so check that the backup matches the source rather
    # than that it has contents
    source_files = {os.path.relpath(filename, data)
                    for filename in glob.glob(f'{data}/**/*', recursive=True)}
    backup_files = {os.path.relpath(filename, data_backup)
                    for filename in glob.glob(f'{data_backup}/**/*', recursive=True)}
    assert source_files == backup_files, \
           f'data backup is missing {source_files - backup_files}'

def clear():

    # clear the old odb and data dir
    subprocess.run('odbedit -c "rmdir /Shifts"', shell=True)
    subprocess.run('odbedit -c "rmdir /Custom/ShiftSchedule"', shell=True)

    # a test whose setup_odb died leaves no data dir, and every later test would then fail
    # here rather than at whatever actually went wrong
    if os.path.isdir(data):
        shutil.rmtree(data)

def restore():
    
    # delete stuff from testing
    clear()

    # restore old files. this is the only thing standing between the suite and a wiped
    # experiment, so check the output: odbedit reports a failed load as text and still
    # exits 0, so a return code check here would pass no matter what happened
    print(f'Restoring {odb_backup}')
    output = odbedit(f'load {odb_backup}')
    assert ',ERROR]' not in output, \
        f'Failed to reload the ODB backup "{odb_backup}" - the live /Shifts tree is still ' \
        f'in its test state and has to be restored by hand. odbedit said:\n{output}'

    print(f'Restoring {data_backup}')
    shutil.copytree(data_backup, data)

    # the backup was taken from a live experiment, so whatever it held has to come back
    assert os.path.isdir(data), f'Data directory "{data}" was not restored'
    if backed_up_shifts:
        assert odb_exists('/Shifts'), '/Shifts is missing after restoring the ODB backup'

def rgb2hex(rgbstring):
    """rgb string to hex values"""

    # get rgb as tuple
    rgbstring = rgbstring.replace('rgb', '')
    rgbstring = rgbstring.replace('a', '')
    rgbstring = rgbstring.replace('(', '')
    rgbstring = rgbstring.replace(')', '')
    rgb = [int(i.strip()) for i in rgbstring.split(',')]
    return f'#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}'

## FIXTURES =================================================================

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    """Widen the browser window.

    The availability table is wider than the default viewport and sits in a
    horizontally scrolling div, so a narrow window makes click and drag scroll partway
    through the gesture."""

    return {**browser_context_args, 'viewport': {'width': 1920, 'height': 1080}}

@pytest.fixture(scope="session")
def client():
    """One midas connection for the whole run

    Session scoped deliberately: connecting and disconnecting once per test meant over a
    hundred cycles in a single process, which midas's own python client documents as an
    unusual case, and the suite would intermittently die with "invalid database handle" or
    a hard abort inside an odb call.

    It lives here rather than in fixtures.py because that module is star-imported, which
    would give every test module its own copy of the fixture - and midas allows only one
    connected client per process.
    """
    client = midas.client.MidasClient("test_shiftschedule")
    yield client
    client.disconnect()

@pytest.fixture(autouse=True)
def page_timeout(page):
    """Time limit on finding elements and completing actions.

    Kept well below fixtures.try_again's own timeout. They used to both be 5 s, so one
    blocked locator call consumed the entire retry window and try_again degenerated to a
    single attempt for exactly the failures it exists to ride out."""

    page.set_default_timeout(1500)

@pytest.fixture(scope="session", autouse=True)
def clean_slate():
    backup()

    yield

    restore()