"""Tests for dashboard defaults (visitor-IP -> business-unit resolution)."""

import pytest


@pytest.fixture
def businessunit(db):
    from shopdb.core.models import BusinessUnit
    bu = BusinessUnit(businessunit='Materials')
    db.session.add(bu)
    db.session.commit()
    return bu


def test_create_and_resolve_visitor_location(client, db, auth_headers, businessunit):
    """A mapped IP resolves to its business unit; explicit ipaddress param works."""
    created = client.post('/api/dashboarddefaults', json={
        'ipaddress': '10.20.30.40',
        'businessunitid': businessunit.businessunitid,
        'description': 'Materials lobby kiosk',
    }, headers=auth_headers)
    assert created.status_code == 201, created.get_json()

    resolved = client.get('/api/dashboarddefaults/visitor-location?ipaddress=10.20.30.40')
    assert resolved.status_code == 200
    data = resolved.get_json()['data']
    assert data['businessunitid'] == businessunit.businessunitid
    assert data['businessunit'] == 'Materials'


def test_unmapped_ip_resolves_to_null(client, db):
    """An unmapped IP returns a null business unit, not an error."""
    resolved = client.get('/api/dashboarddefaults/visitor-location?ipaddress=1.2.3.4')
    assert resolved.status_code == 200
    assert resolved.get_json()['data']['businessunitid'] is None


def test_duplicate_ip_rejected(client, db, auth_headers, businessunit):
    """Mapping the same IP twice is a conflict."""
    payload = {'ipaddress': '10.0.0.9', 'businessunitid': businessunit.businessunitid}
    first = client.post('/api/dashboarddefaults', json=payload, headers=auth_headers)
    assert first.status_code == 201
    dup = client.post('/api/dashboarddefaults', json=payload, headers=auth_headers)
    assert dup.status_code == 409


def test_non_admin_cannot_create(client, db, member_headers, businessunit):
    """A logged-in non-admin is forbidden from creating a mapping (RBAC)."""
    resp = client.post('/api/dashboarddefaults', json={
        'ipaddress': '10.20.30.50',
        'businessunitid': businessunit.businessunitid,
    }, headers=member_headers)
    assert resp.status_code == 403


def test_unauthenticated_cannot_create(client, db, businessunit):
    """No token -> 401 on create."""
    resp = client.post('/api/dashboarddefaults', json={
        'ipaddress': '10.20.30.51',
        'businessunitid': businessunit.businessunitid,
    })
    assert resp.status_code == 401


def test_admin_can_update_and_delete(client, db, auth_headers, businessunit):
    """Admin can PUT and DELETE a mapping."""
    created = client.post('/api/dashboarddefaults', json={
        'ipaddress': '10.20.30.60',
        'businessunitid': businessunit.businessunitid,
    }, headers=auth_headers)
    assert created.status_code == 201
    default_id = created.get_json()['data']['dashboarddefaultid']

    updated = client.put(f'/api/dashboarddefaults/{default_id}',
                         json={'description': 'moved'}, headers=auth_headers)
    assert updated.status_code == 200

    deleted = client.delete(f'/api/dashboarddefaults/{default_id}',
                            headers=auth_headers)
    assert deleted.status_code == 200


def test_non_admin_cannot_update_or_delete(client, db, auth_headers,
                                           member_headers, businessunit):
    """A non-admin is forbidden from PUT and DELETE."""
    created = client.post('/api/dashboarddefaults', json={
        'ipaddress': '10.20.30.70',
        'businessunitid': businessunit.businessunitid,
    }, headers=auth_headers)
    assert created.status_code == 201
    default_id = created.get_json()['data']['dashboarddefaultid']

    put_resp = client.put(f'/api/dashboarddefaults/{default_id}',
                          json={'description': 'nope'}, headers=member_headers)
    assert put_resp.status_code == 403

    del_resp = client.delete(f'/api/dashboarddefaults/{default_id}',
                             headers=member_headers)
    assert del_resp.status_code == 403


def test_lobby_role_needs_no_businessunit_and_resolves_path(client, db, auth_headers):
    """A Lobby mapping needs no business unit; display-role returns its path.

    Posts the RETIRED lowercase spelling on purpose: an existing caller must not
    start failing over a capital, and what comes back is the canonical name.
    """
    created = client.post('/api/dashboarddefaults', json={
        'ipaddress': '10.20.30.99', 'displayrole': 'lobby',
        'description': 'Front lobby TV',
    }, headers=auth_headers)
    assert created.status_code == 201, created.get_json()
    assert created.get_json()['data']['businessunitid'] is None

    resolved = client.get('/api/dashboarddefaults/display-role?ipaddress=10.20.30.99')
    data = resolved.get_json()['data']
    assert data['role'] == 'Lobby'
    assert data['path'] == '/tv'
    assert data['businessunitid'] is None


def test_retired_partskiosk_spelling_maps_to_3dprintroom(client, db, auth_headers):
    # 'partskiosk' was core's name for the room the kiosks call '3DPrintRoom'.
    # It still posts, and stores as the name the kiosk itself would report.
    client.post('/api/dashboarddefaults', json={
        'ipaddress': '10.20.30.77', 'displayrole': 'partskiosk',
    }, headers=auth_headers)
    data = client.get('/api/dashboarddefaults/display-role?ipaddress=10.20.30.77').get_json()['data']
    assert data['role'] == '3DPrintRoom'
    assert data['path'] == '/parts-kiosk'


def test_dashboard_role_requires_businessunit(client, db, auth_headers):
    resp = client.post('/api/dashboarddefaults', json={
        'ipaddress': '10.20.30.55', 'displayrole': 'dashboard',
    }, headers=auth_headers)
    assert resp.status_code == 400
    assert 'location' in resp.get_json()['data']['error']['message'].lower()


def test_fqdn_mapping_create_and_resolve(client, db, auth_headers):
    # A mapping can be keyed by FQDN (no IP), and /display-role resolves it.
    resp = client.post('/api/dashboarddefaults', json={
        'fqdn': 'FABC123.device.geaerospace.net', 'displayrole': 'lobby',
    }, headers=auth_headers)
    assert resp.status_code == 201, resp.data
    assert resp.get_json()['data']['fqdn'] == 'fabc123.device.geaerospace.net'

    # resolve by FQDN (case-insensitive)
    r = client.get('/api/dashboarddefaults/display-role?fqdn=fabc123.device.geaerospace.net')
    body = r.get_json()['data']
    assert body['role'] == 'Lobby'
    assert body['path'] == '/tv'


def test_mapping_requires_fqdn_or_ip(client, db, auth_headers):
    resp = client.post('/api/dashboarddefaults', json={'displayrole': 'lobby'},
                       headers=auth_headers)
    assert resp.status_code == 400
    assert 'fqdn or ipaddress' in resp.get_json()['data']['error']['message'].lower()


def test_invalid_role_rejected(client, db, auth_headers):
    resp = client.post('/api/dashboarddefaults', json={
        'ipaddress': '10.20.30.44', 'displayrole': 'bogus',
    }, headers=auth_headers)
    assert resp.status_code == 400


def test_unmapped_ip_display_role_null(client, db):
    data = client.get('/api/dashboarddefaults/display-role?ipaddress=9.9.9.9').get_json()['data']
    assert data['role'] is None and data['path'] is None


# --- role vocabulary -------------------------------------------------------
#
# The role names are not ours to choose: they are the literal values a person
# types into C:\Enrollment\display-type.txt on the kiosk, which the GE-Enforce
# dispatcher reads. These tests exist because core and the kiosks each used to
# hold their own map, and a display could report a role core could not store.

def test_display_roles_are_the_kiosk_vocabulary():
    from shopdb.core.models.dashboarddefault import DISPLAY_ROLE_PATHS
    assert DISPLAY_ROLE_PATHS == {
        'Dashboard': '/shopfloor',
        'Lobby': '/tv',
        '3DPrintRoom': '/parts-kiosk',
    }


def test_geenforce_display_targets_are_core_roles():
    """The display scope must not carry a second copy of the map."""
    from shopdb.core.models.dashboarddefault import DISPLAY_ROLE_PATHS
    from plugins.geenforce.seed_display_scope import DISPLAY_TYPE_TARGETS
    assert DISPLAY_TYPE_TARGETS is DISPLAY_ROLE_PATHS


def test_normalize_display_role_accepts_any_casing():
    from shopdb.core.models.dashboarddefault import normalize_display_role
    for value in ('Lobby', 'lobby', 'LOBBY', '  Lobby  '):
        assert normalize_display_role(value) == 'Lobby'
    assert normalize_display_role('3dprintroom') == '3DPrintRoom'
    assert normalize_display_role('DASHBOARD') == 'Dashboard'


def test_normalize_display_role_maps_retired_spelling():
    from shopdb.core.models.dashboarddefault import normalize_display_role
    assert normalize_display_role('partskiosk') == '3DPrintRoom'
    assert normalize_display_role('PartsKiosk') == '3DPrintRoom'


def test_normalize_display_role_refuses_to_guess():
    from shopdb.core.models.dashboarddefault import normalize_display_role
    for value in (None, '', '   ', 'bogus', '3d', 'lobbyy'):
        assert normalize_display_role(value) is None


def test_mixed_case_role_stores_canonically(client, db, auth_headers):
    resp = client.post('/api/dashboarddefaults', json={
        'ipaddress': '10.20.30.31', 'displayrole': 'LOBBY',
    }, headers=auth_headers)
    assert resp.status_code == 201, resp.get_json()
    assert resp.get_json()['data']['displayrole'] == 'Lobby'


def test_legacy_stored_value_still_resolves_a_path(db):
    """A row written before the merge keeps working without being rewritten."""
    from shopdb.core.models.dashboarddefault import DashboardDefault
    row = DashboardDefault(ipaddress='10.20.30.32', displayrole='partskiosk')
    db.session.add(row)
    db.session.flush()
    assert row.displaypath == '/parts-kiosk'
