"""GE-Enforce P2 admin CRUD: scopes, entries, reorder, simulate, publish cycle.

All admin endpoints are JWT + geenforce.manage/publish gated (the admin user
holds both). Entry payloads use the manifest Applications[] shape.
"""


def _create_scope(client, auth_headers, name='gea-shopfloor-test'):
    resp = client.post('/api/geenforce/scopes',
                       json={'scopename': name, 'phase': 'runtime',
                             'description': 'test scope'},
                       headers=auth_headers)
    assert resp.status_code == 201, resp.get_json()
    return resp.get_json()['data']['scopeid']


def _add_entry(client, auth_headers, scopeid, entry):
    resp = client.post(f'/api/geenforce/scopes/{scopeid}/entries',
                       json=entry, headers=auth_headers)
    assert resp.status_code == 201, resp.get_json()
    return resp.get_json()['data']['entryid']


def test_scope_crud(client, db, auth_headers):
    scopeid = _create_scope(client, auth_headers)

    got = client.get(f'/api/geenforce/scopes/{scopeid}', headers=auth_headers)
    assert got.status_code == 200
    assert got.get_json()['data']['scopename'] == 'gea-shopfloor-test'

    upd = client.put(f'/api/geenforce/scopes/{scopeid}',
                     json={'description': 'updated', 'computertypeid': 3},
                     headers=auth_headers)
    assert upd.get_json()['data']['computertypeid'] == 3

    dele = client.delete(f'/api/geenforce/scopes/{scopeid}', headers=auth_headers)
    assert dele.status_code == 200
    assert client.get(f'/api/geenforce/scopes/{scopeid}',
                      headers=auth_headers).status_code == 404


def test_duplicate_scope_rejected(client, db, auth_headers):
    _create_scope(client, auth_headers, 'gea-shopfloor-dup')
    resp = client.post('/api/geenforce/scopes',
                       json={'scopename': 'gea-shopfloor-dup', 'phase': 'runtime'},
                       headers=auth_headers)
    assert resp.status_code == 400


def test_entry_crud_and_reorder(client, db, auth_headers):
    scopeid = _create_scope(client, auth_headers)
    a = _add_entry(client, auth_headers, scopeid,
                   {'Name': 'Alpha', 'Type': 'MSI', 'Installer': 'apps/a.msi'})
    b = _add_entry(client, auth_headers, scopeid,
                   {'Name': 'Beta', 'Type': 'PS1', 'Script': 'scripts/b.ps1'})

    # Update Alpha: change type + add detection.
    upd = client.put(f'/api/geenforce/entries/{a}',
                     json={'Name': 'Alpha', 'Type': 'EXE', 'Installer': 'apps/a.exe',
                           'DetectionMethod': 'FileVersion',
                           'DetectionPath': 'C:\\a.exe', 'DetectionValue': '1.0'},
                     headers=auth_headers)
    assert upd.status_code == 200
    assert upd.get_json()['data']['Type'] == 'EXE'
    assert upd.get_json()['data']['DetectionMethod'] == 'FileVersion'

    # Reorder: Beta first.
    reo = client.put(f'/api/geenforce/scopes/{scopeid}/entries/reorder',
                     json={'order': [b, a]}, headers=auth_headers)
    assert reo.status_code == 200
    entries = client.get(f'/api/geenforce/scopes/{scopeid}',
                         headers=auth_headers).get_json()['data']['entries']
    assert [e['Name'] for e in entries] == ['Beta', 'Alpha']

    # Delete Beta.
    assert client.delete(f'/api/geenforce/entries/{b}',
                         headers=auth_headers).status_code == 200
    entries = client.get(f'/api/geenforce/scopes/{scopeid}',
                         headers=auth_headers).get_json()['data']['entries']
    assert [e['Name'] for e in entries] == ['Alpha']


def test_entry_edit_preserves_full_fidelity(client, db, auth_headers):
    """Editing an entry with the complete payload keeps InUseCheck ExePath +
    timeout, LogFile, and preinstall flags (the UI now sends all of them)."""
    scopeid = _create_scope(client, auth_headers)
    rich = {
        'Name': 'PC-DMIS', 'Type': 'MSI', 'Installer': 'apps/pcdmis.msi',
        'LogFile': 'C:\\Logs\\pcdmis.log',
        'PreEnrollment': True, 'PCTypesStrict': True,
        'InUseCheck': {'Behavior': 'CloseAndReopen', 'Processes': [
            {'Name': 'PCDLRN', 'ExePath': 'C:\\PCDLRN.exe',
             'GracefulCloseTimeoutSec': 15}]},
    }
    entryid = _add_entry(client, auth_headers, scopeid, rich)

    # Re-send the same full payload (what the editor now does) and confirm
    # nothing is stripped.
    upd = client.put(f'/api/geenforce/entries/{entryid}', json=rich,
                     headers=auth_headers)
    assert upd.status_code == 200
    entry = upd.get_json()['data']
    assert entry['LogFile'] == 'C:\\Logs\\pcdmis.log'
    assert entry['PreEnrollment'] is True
    assert entry['PCTypesStrict'] is True
    proc = entry['InUseCheck']['Processes'][0]
    assert proc['ExePath'] == 'C:\\PCDLRN.exe'
    assert proc['GracefulCloseTimeoutSec'] == 15


def test_entry_curated_app_link(client, db, auth_headers):
    """An entry can be linked to an existing Application; the link is shopdb
    metadata and must NOT leak into the manifest JSON (parity/enforcement)."""
    from shopdb.core.models import Application
    app_row = Application(appname='eDNC (tracked)')
    db.session.add(app_row)
    db.session.commit()
    appid = app_row.appid

    scopeid = _create_scope(client, auth_headers)
    created = client.post(f'/api/geenforce/scopes/{scopeid}/entries',
                          json={'Name': 'eDNC install', 'Type': 'MSI',
                                'appid': appid}, headers=auth_headers)
    assert created.status_code == 201, created.get_json()
    data = created.get_json()['data']
    assert data['appid'] == appid
    assert data['appname'] == 'eDNC (tracked)'

    # the link is NOT in the rendered manifest (would break the engine contract)
    manifest = client.get(f'/api/geenforce/scopes/{scopeid}/preview',
                          headers=auth_headers).get_json()['data']['manifest']
    entry_json = manifest['Applications'][0]
    assert 'appid' not in entry_json and 'appname' not in entry_json

    # unlink
    updated = client.put(f"/api/geenforce/entries/{data['entryid']}",
                         json={'Name': 'eDNC install', 'Type': 'MSI', 'appid': None},
                         headers=auth_headers)
    assert updated.get_json()['data']['appid'] is None

    # picker lists the app
    picker = client.get('/api/geenforce/applications',
                        headers=auth_headers).get_json()['data']
    assert any(a['appname'] == 'eDNC (tracked)' for a in picker)


def test_entry_nonnumeric_appid_ignored(client, db, auth_headers):
    """A non-numeric appid must be ignored (link stays null), not 500. Matches
    the _apply_app_link docstring contract."""
    scopeid = _create_scope(client, auth_headers)
    created = client.post(f'/api/geenforce/scopes/{scopeid}/entries',
                          json={'Name': 'x', 'Type': 'MSI', 'appid': 'notanumber'},
                          headers=auth_headers)
    assert created.status_code == 201, created.get_json()
    assert created.get_json()['data']['appid'] is None


def test_invalid_entry_type_rejected(client, db, auth_headers):
    scopeid = _create_scope(client, auth_headers)
    resp = client.post(f'/api/geenforce/scopes/{scopeid}/entries',
                       json={'Name': 'Bad', 'Type': 'NOTATYPE'},
                       headers=auth_headers)
    assert resp.status_code == 400


def test_reorder_rejects_foreign_ids(client, db, auth_headers):
    scopeid = _create_scope(client, auth_headers)
    a = _add_entry(client, auth_headers, scopeid,
                   {'Name': 'Alpha', 'Type': 'MSI'})
    resp = client.put(f'/api/geenforce/scopes/{scopeid}/entries/reorder',
                      json={'order': [a, 9999]}, headers=auth_headers)
    assert resp.status_code == 400


def test_simulate_cmm_version_gate(client, db, auth_headers):
    scopeid = _create_scope(client, auth_headers, 'gea-shopfloor-cmm')
    _add_entry(client, auth_headers, scopeid,
               {'Name': 'CLM (untagged)', 'Type': 'MSI', 'Installer': 'apps/clm.msi'})
    _add_entry(client, auth_headers, scopeid,
               {'Name': 'PC-DMIS 2016', 'Type': 'MSI', 'Installer': 'apps/16.msi',
                '_CmmVersion': '2016'})
    _add_entry(client, auth_headers, scopeid,
               {'Name': 'PC-DMIS 2019', 'Type': 'MSI', 'Installer': 'apps/19.msi',
                '_CmmVersion': '2019'})

    sim = client.get(f'/api/geenforce/scopes/{scopeid}/simulate?cmmversion=2019',
                     headers=auth_headers)
    assert sim.status_code == 200
    data = sim.get_json()['data']
    assert data['applied'] == ['CLM (untagged)', 'PC-DMIS 2019']
    filtered = {f['name']: f['filteredby'] for f in data['filtered']}
    assert filtered['PC-DMIS 2016'] == ['_CmmVersion']


def test_duplicate_entry_name_is_400_not_500(client, db, auth_headers):
    scopeid = _create_scope(client, auth_headers)
    _add_entry(client, auth_headers, scopeid, {'Name': 'Alpha', 'Type': 'MSI'})
    dup = client.post(f'/api/geenforce/scopes/{scopeid}/entries',
                      json={'Name': 'Alpha', 'Type': 'PS1'}, headers=auth_headers)
    assert dup.status_code == 400


def test_simulate_pctypesstrict_disables_alias(client, db, auth_headers):
    """A collections-only strict entry must NOT match a nocollections PC via the
    shared Standard alias group. PCTypesStrict is preinstall-only, so the scope
    must be the preinstall phase (the runtime engine ignores strict)."""
    resp = client.post('/api/geenforce/scopes',
                       json={'scopename': 'preinstall', 'phase': 'preinstall'},
                       headers=auth_headers)
    assert resp.status_code == 201, resp.get_json()
    scopeid = resp.get_json()['data']['scopeid']
    _add_entry(client, auth_headers, scopeid,
               {'Name': 'UDC (strict)', 'Type': 'EXE', 'Installer': 'apps/udc.exe',
                'PCTypes': ['gea-shopfloor-collections'], 'PCTypesStrict': True})
    _add_entry(client, auth_headers, scopeid,
               {'Name': 'UDC (loose)', 'Type': 'EXE', 'Installer': 'apps/udc.exe',
                'PCTypes': ['gea-shopfloor-collections']})

    sim = client.get(
        f'/api/geenforce/scopes/{scopeid}/simulate?pctype=gea-shopfloor-nocollections',
        headers=auth_headers).get_json()['data']
    # loose entry matches via the Standard alias group; strict entry does not.
    assert sim['applied'] == ['UDC (loose)']
    filtered = {f['name']: f['filteredby'] for f in sim['filtered']}
    assert filtered['UDC (strict)'] == ['PCTypes']


def test_simulate_machine_number_gate(client, db, auth_headers):
    scopeid = _create_scope(client, auth_headers, 'gea-shopfloor-collections')
    _add_entry(client, auth_headers, scopeid,
               {'Name': 'Okuma cred', 'Type': 'PS1', 'Script': 'scripts/o.ps1',
                'TargetMachineNumbers': ['3201', '3202']})
    _add_entry(client, auth_headers, scopeid,
               {'Name': 'Everyone', 'Type': 'PS1', 'Script': 'scripts/e.ps1'})

    match = client.get(
        f'/api/geenforce/scopes/{scopeid}/simulate?machinenumber=3201',
        headers=auth_headers).get_json()['data']
    assert set(match['applied']) == {'Okuma cred', 'Everyone'}

    nomatch = client.get(
        f'/api/geenforce/scopes/{scopeid}/simulate?machinenumber=9999',
        headers=auth_headers).get_json()['data']
    assert nomatch['applied'] == ['Everyone']


def test_publish_versions_rollback(client, db, auth_headers):
    scopeid = _create_scope(client, auth_headers, 'gea-shopfloor-cmm')
    _add_entry(client, auth_headers, scopeid, {'Name': 'Alpha', 'Type': 'MSI'})

    pub1 = client.post(f'/api/geenforce/scopes/{scopeid}/publish',
                       json={'notes': 'v1'}, headers=auth_headers)
    assert pub1.status_code == 201
    assert pub1.get_json()['data']['versionnumber'] == 1

    _add_entry(client, auth_headers, scopeid, {'Name': 'Beta', 'Type': 'PS1'})
    pub2 = client.post(f'/api/geenforce/scopes/{scopeid}/publish',
                       json={'notes': 'v2'}, headers=auth_headers)
    assert pub2.get_json()['data']['versionnumber'] == 2

    versions = client.get(f'/api/geenforce/scopes/{scopeid}/versions',
                          headers=auth_headers).get_json()['data']
    assert [v['versionnumber'] for v in versions] == [2, 1]
    assert versions[0]['iscurrent'] is True

    # v2 manifest has both entries.
    v2doc = client.get(f'/api/geenforce/scopes/{scopeid}/versions/2',
                       headers=auth_headers).get_json()['data']['manifest']
    assert [e['Name'] for e in v2doc['Applications']] == ['Alpha', 'Beta']

    # Rollback to v1.
    rb = client.post(f'/api/geenforce/scopes/{scopeid}/rollback',
                     json={'versionnumber': 1}, headers=auth_headers)
    assert rb.status_code == 200
    versions = client.get(f'/api/geenforce/scopes/{scopeid}/versions',
                          headers=auth_headers).get_json()['data']
    current = next(v for v in versions if v['iscurrent'])
    assert current['versionnumber'] == 1
