"""Cover the gea-shopfloor-display authoring seed.

Running seed_display_scope must produce the display scope with the expected
entry count and types: four Registry drift-heal entries (Edge kiosk relaunch
policies) plus one inline PS1 dispatcher. Also checks the dispatcher payload is
stored inline and the display-type map drives the dispatcher script.
"""

from plugins.geenforce.models import (
    ManifestScope, ManifestEntry, ManifestPayload,
)
from plugins.geenforce.seed_display_scope import (
    seed_display_scope, build_display_manifest, build_dispatcher_script,
    build_watchdog_script, build_forceedgeupdate_script,
    DISPLAY_TYPE_TARGETS, SCOPE_NAME, DISPATCHER_FILENAME, ALWAYSON_FILENAME,
    FORCE_EDGE_UPDATE_FILENAME, FORCE_EDGE_UPDATE_MARKER,
)


def test_seed_creates_display_scope(db):
    summary = seed_display_scope()

    scope = ManifestScope.query.filter_by(
        scopename=SCOPE_NAME, phase='runtime').first()
    assert scope is not None
    # A plain runtime scope, not the common scope. Displays are self-sufficient
    # and do NOT inherit common.
    assert scope.iscommon is False

    # The self-updating client module (File), then four Registry drift-heal
    # entries, then four inline PS1 (dispatcher, watchdog, one-shot Edge
    # force-update, always-on).
    assert summary['entrycount'] == 9
    assert summary['entrytypes'] == ['File', 'Registry', 'Registry', 'Registry',
                                     'Registry', 'PS1', 'PS1', 'PS1', 'PS1']


def test_registry_entries_use_valuematches_detection(db):
    seed_display_scope()

    registry_entries = ManifestEntry.query.filter_by(
        entrytype='Registry').order_by(ManifestEntry.sortorder).all()
    assert len(registry_entries) == 4
    regnames = {entry.regname for entry in registry_entries}
    assert regnames == {
        'RelaunchNotification', 'RelaunchNotificationPeriod',
        'RelaunchHeadsUpPeriod', 'RelaunchWindow',
    }
    for entry in registry_entries:
        assert entry.detectionmethod == 'ValueMatches'
        assert entry.regpath == 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Edge'
        # Detection targets the same value it writes, coerced to string.
        assert entry.detectionname == entry.regname
        assert entry.detectionvalue is not None


def test_dispatcher_is_inline_and_data_driven(db):
    summary = seed_display_scope()

    dispatcher = ManifestEntry.query.filter_by(
        payloadref=DISPATCHER_FILENAME).one()
    assert dispatcher.payloadsource == 'inline'
    assert dispatcher.payloadref == DISPATCHER_FILENAME
    assert dispatcher.payloadsha256 == summary['dispatchersha256']

    payload = ManifestPayload.query.filter_by(
        entryid=dispatcher.entryid).one()
    scripttext = payload.payloadbytes.decode('utf-8')
    # The data-driven map surfaces every display-type and its target route.
    for display_type, route in DISPLAY_TYPE_TARGETS.items():
        assert display_type in scripttext
        assert route in scripttext
    assert 'display-type.txt' in scripttext
    # H3: SYSTEM writes an all-users Startup shortcut (CreateShortcut/.Save),
    # rather than Start-Process-ing Edge (which would open invisibly in session 0).
    assert 'CreateShortcut' in scripttext
    assert '.Save()' in scripttext
    assert 'Startup' in scripttext
    # H4: real base URL, not the localhost placeholder.
    assert 'localhost' not in scripttext


def test_alwayson_entry_is_inline(db):
    summary = seed_display_scope()

    alwayson = ManifestEntry.query.filter_by(
        payloadref=ALWAYSON_FILENAME).one()
    assert alwayson.entrytype == 'PS1'
    assert alwayson.payloadsource == 'inline'
    assert alwayson.detectionmethod == 'Always'
    assert alwayson.payloadsha256 == summary['alwaysonsha256']

    payload = ManifestPayload.query.filter_by(entryid=alwayson.entryid).one()
    scripttext = payload.payloadbytes.decode('utf-8')
    assert 'powercfg' in scripttext
    assert 'monitor-timeout-ac 0' in scripttext


def test_seed_publish_freezes_a_version(db):
    summary = seed_display_scope(publish=True)
    assert summary['publishedversion'] == 1

    scope = ManifestScope.query.filter_by(
        scopename=SCOPE_NAME, phase='runtime').first()
    published = scope.publishedversions.filter_by(iscurrent=True).first()
    assert published is not None
    assert published.versionnumber == 1
    assert '"Version": "2.0"' in published.manifestjson


def test_seed_draft_is_idempotent(db):
    first = seed_display_scope()
    second = seed_display_scope()
    # Re-running rebuilds the draft to the same shape (same scope, same count).
    assert first['scopeid'] == second['scopeid']
    assert first['entrycount'] == second['entrycount']
    assert first['dispatchersha256'] == second['dispatchersha256']
    assert first['alwaysonsha256'] == second['alwaysonsha256']
    assert first['watchdogsha256'] == second['watchdogsha256']
    assert first['forceupdatesha256'] == second['forceupdatesha256']
    assert first['clientmodulesha256'] == second['clientmodulesha256']

    entries = ManifestEntry.query.filter_by(scopeid=second['scopeid']).all()
    assert len(entries) == 9
    # Exactly four inline payloads (dispatcher + watchdog + force-update +
    # always-on) exist
    # after a rebuild, not more - a payload-count invariant. (The underlying
    # re-publish FK crash only reproduces on MySQL, which enforces the
    # manifestpayloads FK; it was verified there directly. SQLite does not
    # enforce it.)
    assert ManifestPayload.query.count() == 5


def test_build_manifest_has_no_smb_exe_payloads(db):
    manifest = build_display_manifest()
    # No entry ships an EXE/MSI/File payload from a share: the only payload is
    # the inline dispatcher; everything else is a Registry policy heal.
    for entry in manifest['Applications']:
        assert entry.get('Installer') is None
        # Every payload-bearing entry is delivered INLINE over HTTPS, never from
        # a share: a display has no SFLD credentials at all.
        if entry['Type'] in ('PS1', 'File'):
            assert entry.get('PayloadSource') == 'inline'
        else:
            assert entry.get('Source') is None


def test_dispatcher_script_is_ascii():
    # Plain ASCII only (no smart quotes / em-dashes) so the naming gate stays
    # green and the on-PC script parses cleanly.
    build_dispatcher_script().encode('ascii')


def test_watchdog_entry_is_present_and_inline(db):
    """The Startup shortcut runs once at logon, so an Edge update/crash leaves
    the display dead until the next logon. The watchdog is what recovers it."""
    manifest = build_display_manifest()
    entry = next(a for a in manifest['Applications']
                 if a['Name'] == 'Display kiosk watchdog (relaunch Edge)')
    assert entry['Type'] == 'PS1'
    assert entry['PayloadSource'] == 'inline'
    assert entry['DetectionMethod'] == 'Always'


def test_watchdog_script_registers_an_interactive_task():
    """A SYSTEM task would start Edge in session 0 where nobody can see it, so
    the principal must be the interactive Users group."""
    script = build_watchdog_script()
    assert "New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545'" in script
    assert 'ShopDB Kiosk Watchdog' in script
    # The MaxValue repetition trap the kiosk installer documents. Checked
    # against CODE lines only - the script comments the trap on purpose, so a
    # naive substring test would fail on its own warning.
    code = [line for line in script.splitlines()
            if not line.lstrip().startswith('#')]
    assert not any('RepetitionDuration' in line for line in code)


def test_watchdog_matches_on_the_command_line_not_the_image_name():
    """Edge spawns many msedge.exe children; only the parent carries --kiosk.
    Testing 'is msedge running' would let a stray renderer mask a dead kiosk."""
    script = build_watchdog_script()
    assert "CommandLine -match '--kiosk'" in script


def test_watchdog_does_nothing_without_a_kiosk_shortcut():
    """It must never invent a browser launch on a PC that is not a kiosk."""
    script = build_watchdog_script()
    assert 'ShopDB Kiosk.lnk' in script
    assert 'shopdb-kiosk-lastlaunch' in script  # debounce stamp


def test_watchdog_requires_a_visible_window_not_just_a_process():
    """Edge can update and leave a --kiosk process behind with no window. The
    first version tested the command line alone, so it saw 'kiosk is up' and
    never relaunched - observed on a real display."""
    script = build_watchdog_script()
    assert 'MainWindowHandle' in script


def test_watchdog_kills_windowless_kiosk_before_relaunching():
    """Leaving the orphan would satisfy the next cycle's check again, and two
    browsers would fight over the display."""
    script = build_watchdog_script()
    assert 'Stop-Process' in script
    assert 'windowless' in script


def test_watchdog_refuses_to_run_as_system():
    """MainWindowHandle is session-scoped: SYSTEM reads 0 for a healthy kiosk,
    so a mis-registered task would kill and relaunch the display every cycle."""
    script = build_watchdog_script()
    assert 'IsSystem' in script


def _forceupdate_entry():
    return next(a for a in build_display_manifest()['Applications']
                if a['Name'].startswith('Force pending Edge update'))


def test_forceupdate_is_a_one_shot_marker_entry(db):
    """MarkerFile is what makes it one-shot. With Always it would kill Edge on
    every enforce cycle forever, which is a kiosk that never stays up."""
    entry = _forceupdate_entry()
    assert entry['Type'] == 'PS1'
    assert entry['PayloadSource'] == 'inline'
    assert entry['DetectionMethod'] == 'MarkerFile'
    # The engine only writes the marker when DetectionPath is set; without it
    # the entry silently degrades to running every cycle.
    assert entry['DetectionPath'] == FORCE_EDGE_UPDATE_MARKER


def test_forceupdate_runs_after_the_watchdog_is_registered(db):
    """It stops Edge and relies on the watchdog to bring the kiosk back, so on a
    display seeing both for the first time the watchdog must come first."""
    names = [a['Name'] for a in build_display_manifest()['Applications']]
    watchdog = names.index('Display kiosk watchdog (relaunch Edge)')
    forceupdate = next(i for i, n in enumerate(names)
                       if n.startswith('Force pending Edge update'))
    assert watchdog < forceupdate


def test_forceupdate_payload_is_stored(db):
    """An entry whose payload never stored would enforce nothing at all."""
    summary = seed_display_scope()
    payload = ManifestPayload.query.filter_by(
        filename=FORCE_EDGE_UPDATE_FILENAME).one()
    assert payload.payloadsha256 == summary['forceupdatesha256']


def test_forceupdate_script_exits_zero_so_the_marker_is_written():
    """The engine writes the marker only on a 0 exit. A script that failed on
    'no Edge running' would never mark done and would re-kill Edge forever."""
    script = build_forceedgeupdate_script()
    assert script.rstrip().endswith('exit 0')
    assert 'exit 1' not in script


def test_forceupdate_script_does_not_launch_the_browser_itself():
    """SYSTEM runs in session 0, where a launched browser is invisible. Relaunch
    is the watchdog's job, from the Startup shortcut."""
    script = build_forceedgeupdate_script()
    assert 'msedge' in script
    assert 'Start-Process' not in script.split('# Stop every Edge process')[1]


def test_forceupdate_script_is_ascii():
    build_forceedgeupdate_script().encode('ascii')


def _client_entry():
    return next(a for a in build_display_manifest()['Applications']
                if a['Name'].startswith('GE-Enforce client module'))


def test_client_module_ships_with_a_hash_of_the_bytes_it_ships(db):
    """The detection hash must match the shipped payload exactly. A mismatch
    means every kiosk rewrites the module on every cycle, forever."""
    import hashlib
    from plugins.geenforce.seed_display_scope import read_client_module

    entry = _client_entry()
    assert entry['DetectionMethod'] == 'Hash'
    assert entry['DetectionValue'] == hashlib.sha256(read_client_module()).hexdigest()
    # Detection must look at where the file LANDS, not where it came from.
    assert entry['DetectionPath'] == entry['Destination']


def test_client_module_lands_where_the_installer_puts_it(db):
    """Bootstrap and self-update must write the SAME path, or a kiosk ends up
    running the installer's copy while the enforced one sits beside it."""
    entry = _client_entry()
    assert entry['Destination'].endswith('\\ShopdbEnforceClient.psm1')
    assert 'GE-Enforce' in entry['Destination']


def test_client_module_is_first_so_a_stale_client_refreshes_first(db):
    names = [a['Name'] for a in build_display_manifest()['Applications']]
    assert names[0].startswith('GE-Enforce client module')
