"""Low-toner alert tests.

Invariants: the alert tier boundaries (5% warning, 0% critical), the
fire-once-per-crossing state machine (re-arm on refill), toner-only scope,
and recipient routing to a support team's webhook.
"""

import pytest
from unittest.mock import patch

from shopdb.api import db
from shopdb.core.models import AssetType, Setting, SupportTeam
from plugins.printers.models import PrinterSupplyAlert
from plugins.printers.services.supply_parts import alerttier
from plugins.printers.services.supply_alerts import check_supplies


class FakeZabbix:
    """Stand-in Zabbix service: canned supplies keyed by IP."""

    def __init__(self, byip):
        self._byip = byip
        self.isconfigured = True

    def getsuppliesbyip(self, ip):
        return self._byip.get(ip, [])


@pytest.fixture
def printer_assettype(db):
    at = AssetType(assettype='printer', pluginname='printers',
                   tablename='printers', description='Printers')
    db.session.add(at)
    db.session.commit()
    return at


@pytest.fixture
def ip_comtype(db):
    from shopdb.core.models import CommunicationType
    row = CommunicationType.query.filter_by(comtype='IP').first()
    if not row:
        row = CommunicationType(comtype='IP', description='IP address')
        db.session.add(row)
        db.session.commit()
    return row


@pytest.fixture
def network_printer(client, auth_headers, printer_assettype, ip_comtype):
    """A network printer at 10.0.0.5 with a black toner, via the real API."""
    resp = client.post('/api/printers', json={
        'assetnumber': 'CSF01-LaserJet',
        'hostname': 'wjprn05',
        'ipaddress': '10.0.0.5',
    }, headers=auth_headers)
    assert resp.status_code == 201, resp.get_json()
    return '10.0.0.5'


def test_alerttier_boundaries():
    assert alerttier(50) == 'ok'
    assert alerttier(6) == 'ok'
    assert alerttier(5) == 'warning'
    assert alerttier(1) == 'warning'
    assert alerttier(0) == 'critical'


def _toner(level, color='black'):
    return {'name': f'{color} toner', 'level': level, 'color': color}


def test_warning_crossing_fires_once_and_rearms(app, db, network_printer):
    ip = network_printer
    with app.app_context():
        # 20% -> ok, no alert
        with patch('shopdb.api.send_webhook') as hook1, \
             patch('shopdb.api.send_email'):
            check_supplies(FakeZabbix({ip: [_toner(20)]}))
            assert not hook1.called

        # 4% -> warning, alert fires
        with patch('shopdb.api.send_webhook') as hook2, \
             patch('shopdb.api.send_email'):
            check_supplies(FakeZabbix({ip: [_toner(4)]}))
            assert hook2.called

        # still 3% -> already warning, no repeat
        with patch('shopdb.api.send_webhook') as hook3, \
             patch('shopdb.api.send_email'):
            check_supplies(FakeZabbix({ip: [_toner(3)]}))
            assert not hook3.called

        # refill to 80% -> re-arm (state back to ok), no alert
        with patch('shopdb.api.send_webhook') as hook4, \
             patch('shopdb.api.send_email'):
            check_supplies(FakeZabbix({ip: [_toner(80)]}))
            assert not hook4.called
        row = PrinterSupplyAlert.query.filter_by(supplykey='black').first()
        assert row.lasttier == 'ok'

        # drop to 2% again -> warning fires again after the re-arm
        with patch('shopdb.api.send_webhook') as hook5, \
             patch('shopdb.api.send_email'):
            check_supplies(FakeZabbix({ip: [_toner(2)]}))
            assert hook5.called


def test_direct_to_critical_fires_critical(app, db, network_printer):
    ip = network_printer
    with app.app_context():
        with patch('shopdb.api.send_webhook') as hook, \
             patch('shopdb.api.send_email'):
            check_supplies(FakeZabbix({ip: [_toner(0)]}))
            assert hook.called
            subject = hook.call_args.args[0]
            assert 'CRITICAL' in subject
        row = PrinterSupplyAlert.query.filter_by(supplykey='black').first()
        assert row.lasttier == 'critical'


def test_alerttier_respects_custom_thresholds():
    # warning at <=20, critical at <=8
    assert alerttier(25, warning=20, critical=8) == 'ok'
    assert alerttier(20, warning=20, critical=8) == 'warning'
    assert alerttier(8, warning=20, critical=8) == 'critical'


def test_custom_warning_threshold_setting_fires(app, db, network_printer):
    ip = network_printer
    with app.app_context():
        Setting.set('printers_alert_warning_threshold', '20')
        # 15% would be 'ok' under the default 5% warning, but fires at 20%
        with patch('shopdb.api.send_webhook') as hook, \
             patch('shopdb.api.send_email'):
            check_supplies(FakeZabbix({ip: [_toner(15)]}))
            assert hook.called
        row = PrinterSupplyAlert.query.filter_by(supplykey='black').first()
        assert row.lasttier == 'warning'


def test_non_toner_supply_ignored(app, db, network_printer):
    ip = network_printer
    with app.app_context():
        waste = {'name': 'waste toner box', 'level': 1, 'color': 'none'}
        with patch('shopdb.api.send_email') as email, \
             patch('shopdb.api.send_webhook'):
            check_supplies(FakeZabbix({ip: [waste]}))
            assert not email.called


def test_routes_to_support_team_webhook(app, db, network_printer):
    ip = network_printer
    with app.app_context():
        team = SupportTeam(teamname='Print Team',
                           webhookurl='https://teams.example/print')
        db.session.add(team)
        db.session.commit()
        Setting.set('printers_alert_supportteamid', str(team.supportteamid))

        with patch('shopdb.api.send_webhook') as webhook, \
             patch('shopdb.api.send_email'):
            check_supplies(FakeZabbix({ip: [_toner(3)]}))
            assert webhook.called
            assert webhook.call_args.kwargs['url'] == 'https://teams.example/print'
