"""Dashboard card: PCs that were reporting and have stopped.

Deliberately narrow. A PC that never reported at all is usually a record made by
hand or imported, not a bay that broke, and a PC that is not In Use is silent on
purpose - that is the status doing its job. Both would bury the real signal: a
machine that was working, is not now, and nobody has marked as anything else.
"""

from datetime import datetime, timedelta, timezone

from shopdb.core.models import Asset, AssetStatus, AssetType, Setting

URL = '/api/computers/dashboard/quiet'


def _now():
    return datetime.now(timezone.utc).replace(tzinfo=None)


def _inuse(db):
    """The card joins on the status NAME, so a bare statusid will not do."""
    status = AssetStatus.query.filter_by(status='In Use').first()
    if not status:
        status = AssetStatus(status='In Use', isactive=True)
        db.session.add(status)
        db.session.flush()
    return status


def _pc(db, hostname, lastreported='now', isactive=True):
    from plugins.computers.models import Computer

    atype = AssetType.query.filter_by(assettype='computer').first()
    if not atype:
        atype = AssetType(assettype='computer')
        db.session.add(atype)
        db.session.flush()
    asset = Asset(assetnumber=hostname, assettypeid=atype.assettypeid,
                  statusid=_inuse(db).statusid, isactive=isactive)
    db.session.add(asset)
    db.session.flush()
    comp = Computer(assetid=asset.assetid, hostname=hostname)
    if lastreported == 'now':
        comp.lastreporteddate = _now()
    elif lastreported is None:
        comp.lastreporteddate = None
    else:
        comp.lastreporteddate = _now() - timedelta(hours=lastreported)
    db.session.add(comp)
    db.session.commit()
    return comp


def test_a_reporting_pc_is_not_listed(client, db, auth_headers):
    _pc(db, 'BUSYPC')
    assert client.get(URL, headers=auth_headers).get_json()['data'] == []


def test_a_pc_gone_quiet_is_listed_with_how_long(client, db, auth_headers):
    _pc(db, 'QUIETPC', lastreported=72)
    [row] = client.get(URL, headers=auth_headers).get_json()['data']
    assert row['hostname'] == 'QUIETPC'
    assert row['quietdays'] == 3


def test_a_pc_that_never_reported_is_not_listed(client, db, auth_headers):
    """Usually a hand-made or imported record rather than a bay that broke.
    Listing them buries the PCs that actually changed state."""
    _pc(db, 'NEVERPC', lastreported=None)
    assert client.get(URL, headers=auth_headers).get_json()['data'] == []


def test_a_pc_not_in_use_is_not_listed(client, db, auth_headers):
    """A PC in Repair or Retired is silent on purpose - that is the status
    doing its job, not a fault to chase."""
    from shopdb.core.models import AssetStatus
    repair = AssetStatus.query.filter_by(status='In Repair').first()
    if not repair:
        repair = AssetStatus(status='In Repair', isactive=True)
        db.session.add(repair)
        db.session.flush()
    comp = _pc(db, 'SHELVED', lastreported=200)
    comp.asset.statusid = repair.statusid
    db.session.commit()
    assert client.get(URL, headers=auth_headers).get_json()['data'] == []


def test_the_longest_silence_comes_first(client, db, auth_headers):
    _pc(db, 'DAY2', lastreported=48)
    _pc(db, 'DAY9', lastreported=216)
    rows = client.get(URL, headers=auth_headers).get_json()['data']
    assert [r['hostname'] for r in rows] == ['DAY9', 'DAY2']


def test_the_window_is_a_setting_not_a_hardcode(client, db, auth_headers):
    """Every site will disagree with 24 hours (ADR-015)."""
    _pc(db, 'OVERNIGHT', lastreported=10)
    assert client.get(URL, headers=auth_headers).get_json()['data'] == []

    db.session.add(Setting(key='computers_quietreporthours', value='4'))
    db.session.commit()
    rows = client.get(URL, headers=auth_headers).get_json()['data']
    assert [r['hostname'] for r in rows] == ['OVERNIGHT']


def test_a_bad_setting_falls_back_rather_than_failing_the_card(client, db,
                                                               auth_headers):
    db.session.add(Setting(key='computers_quietreporthours', value='soon'))
    db.session.commit()
    _pc(db, 'QUIETPC', lastreported=72)
    assert client.get(URL, headers=auth_headers).status_code == 200


def test_a_retired_pc_is_not_nagged_about(client, db, auth_headers):
    """A decommissioned PC is silent on purpose. Listing it would train people
    to ignore the card, which is the failure mode this whole board avoids."""
    _pc(db, 'RETIRED', lastreported=None, isactive=False)
    assert client.get(URL, headers=auth_headers).get_json()['data'] == []


def test_the_card_is_permission_gated(client, db, member_headers):
    assert client.get(URL, headers=member_headers).status_code == 403


def test_the_widget_uses_the_new_contract(app):
    from plugins.computers.plugin import ComputersPlugin

    widget = ComputersPlugin().get_dashboard_widgets()[0]
    assert 'component' not in widget
    assert widget['render'] == 'exceptions'
    assert widget['permission'] == 'computers.view'
    assert widget['empty'] == 'hide'


# =============================================================================
# Machine numbers claimed by two PCs - a fault, unless the devices are modelled
# =============================================================================

SHAREDURL = '/api/computers/dashboard/sharedmachines'


def _machine(db, assetnumber):
    mt = AssetType.query.filter_by(assettype='machine').first()
    if not mt:
        mt = AssetType(assettype='machine')
        db.session.add(mt)
        db.session.flush()
    asset = Asset(assetnumber=assetnumber, assettypeid=mt.assettypeid,
                  statusid=1)
    db.session.add(asset)
    db.session.commit()
    return asset


def _controls(db, pchostname, machineasset, label='collector:machine'):
    from shopdb.core.models import AssetRelationship, RelationshipType
    controls = RelationshipType.query.filter_by(relationshiptype='controls').first()
    if not controls:
        controls = RelationshipType(relationshiptype='controls', isdirectional=True)
        db.session.add(controls)
        db.session.flush()
    comp = _pc(db, pchostname)
    db.session.add(AssetRelationship(
        sourceassetid=comp.assetid, targetassetid=machineasset.assetid,
        relationshiptypeid=controls.relationshiptypeid, label=label,
        isactive=True))
    db.session.commit()
    return comp


def test_one_pc_per_machine_is_not_a_finding(client, db, auth_headers):
    machine = _machine(db, '3015')
    _controls(db, 'ONLYPC', machine)
    assert client.get(SHAREDURL, headers=auth_headers).get_json()['data'] == []


def test_two_pcs_on_one_number_is_reported(client, db, auth_headers):
    machine = _machine(db, '2026')
    _controls(db, 'PCONE', machine)
    _controls(db, 'PCTWO', machine)
    [row] = client.get(SHAREDURL, headers=auth_headers).get_json()['data']
    assert row['machinenumber'] == '2026'
    assert row['pccount'] == 2
    assert 'PCONE' in row['pcs'] and 'PCTWO' in row['pcs']


def test_a_modelled_shared_number_is_not_a_finding(client, db, auth_headers):
    """Part markers legitimately share an operation number. Modelled correctly,
    each device is its own asset filed partof the operation - so the operation
    has child assets, and that is what tells a real fault from correct data."""
    from shopdb.core.models import AssetRelationship, RelationshipType
    operation = _machine(db, '0615')
    _controls(db, 'MARKERPCA', operation)
    _controls(db, 'MARKERPCB', operation)

    partof = RelationshipType(relationshiptype='partof', isdirectional=True)
    db.session.add(partof)
    db.session.flush()
    marker = _machine(db, 'MARKERPCA-PARTMARKER')
    db.session.add(AssetRelationship(
        sourceassetid=marker.assetid, targetassetid=operation.assetid,
        relationshiptypeid=partof.relationshiptypeid,
        label='collector:partmarker', isactive=True))
    db.session.commit()

    assert client.get(SHAREDURL, headers=auth_headers).get_json()['data'] == []


def test_shared_machines_is_permission_gated(client, db, member_headers):
    assert client.get(SHAREDURL, headers=member_headers).status_code == 403
