import json

import pytest
from django.urls import reverse

from django_lumen.models import DiagramPreferences


# helpers

DIAGRAM_URL       = reverse('django-lumen-diagram')
SAVE_PREFS_URL    = reverse('django-lumen-save-preferences')
EXCLUDED_APPS_URL = reverse('django-lumen-excluded-apps')
SETTINGS_URL      = reverse('django-lumen-settings')
HELP_URL          = reverse('django-lumen-help')


# DiagramView

@pytest.mark.django_db
class TestDiagramView:
    def test_anonymous_user_is_redirected(self, client):
        response = client.get(DIAGRAM_URL)
        assert response.status_code == 302

    def test_authenticated_non_staff_gets_403(self, client, regular_user):
        client.force_login(regular_user)
        response = client.get(DIAGRAM_URL)
        assert response.status_code == 403

    def test_inactive_staff_is_redirected(self, client, django_user_model):
        # Django's ModelBackend.get_user() excludes inactive users, so they are
        # treated as AnonymousUser by the auth middleware → redirect, not 403.
        user = django_user_model.objects.create_user(
            username='inactive_staff', password='pw', is_staff=True, is_active=False
        )
        client.force_login(user)
        response = client.get(DIAGRAM_URL)
        assert response.status_code == 302

    def test_staff_user_gets_200(self, staff_client):
        response = staff_client.get(DIAGRAM_URL)
        assert response.status_code == 200

    def test_response_contains_diagram_data(self, staff_client):
        response = staff_client.get(DIAGRAM_URL)
        diagram_data = json.loads(response.context['diagram_data'])
        assert 'models' in diagram_data

    def test_response_contains_prefs(self, staff_client):
        response = staff_client.get(DIAGRAM_URL)
        prefs = json.loads(response.context['prefs'])
        assert 'selected_apps' in prefs
        assert 'excluded_apps' in prefs
        assert 'routing_style' in prefs

    def test_default_prefs_used_when_no_record(self, staff_client, staff_user):
        response = staff_client.get(DIAGRAM_URL)
        prefs = json.loads(response.context['prefs'])
        assert prefs['routing_style'] == 'curved'
        assert 'auth' in prefs['excluded_apps']

    def test_saved_prefs_are_loaded(self, staff_client, staff_user):
        DiagramPreferences.objects.create(
            user=staff_user, routing_style='direct', excluded_apps=['auth'],
        )
        response = staff_client.get(DIAGRAM_URL)
        prefs = json.loads(response.context['prefs'])
        assert prefs['routing_style'] == 'direct'
        assert prefs['excluded_apps'] == ['auth']

    def test_excluded_apps_filter_diagram_data(self, staff_client, staff_user):
        # With 'tests' NOT excluded, tests models should appear in diagram data
        DiagramPreferences.objects.create(
            user=staff_user, excluded_apps=['auth', 'admin', 'contenttypes', 'sessions', 'django_lumen']
        )
        response = staff_client.get(DIAGRAM_URL)
        diagram_data = json.loads(response.context['diagram_data'])
        app_labels = {m['app'] for m in diagram_data['models']}
        assert 'tests' in app_labels

    def test_excluded_app_absent_from_diagram_data(self, staff_client, staff_user):
        DiagramPreferences.objects.create(
            user=staff_user,
            excluded_apps=['auth', 'admin', 'contenttypes', 'sessions', 'django_lumen', 'tests'],
        )
        response = staff_client.get(DIAGRAM_URL)
        diagram_data = json.loads(response.context['diagram_data'])
        app_labels = {m['app'] for m in diagram_data['models']}
        assert 'tests' not in app_labels


# SavePreferencesView

@pytest.mark.django_db
class TestSavePreferencesView:
    def test_anonymous_is_redirected(self, client):
        response = client.post(SAVE_PREFS_URL, content_type='application/json', data='{}')
        assert response.status_code == 302

    def test_non_staff_gets_403(self, client, regular_user):
        client.force_login(regular_user)
        response = client.post(SAVE_PREFS_URL, content_type='application/json', data='{}')
        assert response.status_code == 403

    def test_creates_preferences_record(self, staff_client, staff_user):
        payload = {'selected_apps': ['myapp'], 'excluded_apps': ['auth'], 'routing_style': 'direct'}
        response = staff_client.post(
            SAVE_PREFS_URL, data=json.dumps(payload), content_type='application/json'
        )
        assert response.status_code == 200
        assert response.json() == {'ok': True}
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.selected_apps == ['myapp']
        assert prefs.excluded_apps == ['auth']
        assert prefs.routing_style == 'direct'

    def test_updates_existing_preferences_record(self, staff_client, staff_user):
        DiagramPreferences.objects.create(user=staff_user, routing_style='curved')
        payload = {'selected_apps': [], 'excluded_apps': [], 'routing_style': 'orthogonal'}
        staff_client.post(SAVE_PREFS_URL, data=json.dumps(payload), content_type='application/json')
        assert DiagramPreferences.objects.filter(user=staff_user).count() == 1
        assert DiagramPreferences.objects.get(user=staff_user).routing_style == 'orthogonal'

    def test_partial_payload_only_updates_sent_fields(self, staff_client, staff_user):
        DiagramPreferences.objects.create(user=staff_user, routing_style='direct', exclude_models=[r'^Audit'])
        staff_client.post(SAVE_PREFS_URL, data=json.dumps({'routing_style': 'curved'}), content_type='application/json')
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.routing_style == 'curved'
        assert prefs.exclude_models == [r'^Audit']

    def test_unknown_fields_are_ignored(self, staff_client, staff_user):
        staff_client.post(SAVE_PREFS_URL, data=json.dumps({'routing_style': 'direct', 'evil': 'x'}), content_type='application/json')
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.routing_style == 'direct'
        assert not hasattr(prefs, 'evil')


# ExcludedAppsView (legacy redirect)

@pytest.mark.django_db
class TestExcludedAppsView:
    """Old URL now redirects to the unified Settings page."""
    def test_anonymous_is_redirected(self, client):
        response = client.get(EXCLUDED_APPS_URL)
        assert response.status_code == 302

    def test_non_staff_gets_403(self, client, regular_user):
        client.force_login(regular_user)
        response = client.get(EXCLUDED_APPS_URL)
        assert response.status_code == 403

    def test_staff_get_redirects_to_settings(self, staff_client):
        response = staff_client.get(EXCLUDED_APPS_URL)
        assert response.status_code == 302
        assert 'settings' in response['Location']

    def test_staff_post_redirects_to_settings(self, staff_client):
        response = staff_client.post(EXCLUDED_APPS_URL)
        assert response.status_code == 302
        assert 'settings' in response['Location']


# HelpView

@pytest.mark.django_db
class TestHelpView:
    def test_anonymous_is_redirected(self, client):
        response = client.get(HELP_URL)
        assert response.status_code == 302

    def test_non_staff_gets_403(self, client, regular_user):
        client.force_login(regular_user)
        response = client.get(HELP_URL)
        assert response.status_code == 403

    def test_staff_gets_200(self, staff_client):
        response = staff_client.get(HELP_URL)
        assert response.status_code == 200


# SettingsView

@pytest.mark.django_db
class TestSettingsView:
    def test_anonymous_is_redirected(self, client):
        response = client.get(SETTINGS_URL)
        assert response.status_code == 302

    def test_non_staff_gets_403(self, client, regular_user):
        client.force_login(regular_user)
        response = client.get(SETTINGS_URL)
        assert response.status_code == 403

    def test_staff_gets_200(self, staff_client):
        response = staff_client.get(SETTINGS_URL)
        assert response.status_code == 200

    # Context

    def test_context_has_all_apps(self, staff_client):
        response = staff_client.get(SETTINGS_URL)
        assert 'auth' in response.context['all_apps']
        assert 'tests' in response.context['all_apps']

    def test_context_excluded_apps_from_prefs(self, staff_client, staff_user):
        DiagramPreferences.objects.create(user=staff_user, excluded_apps=['auth', 'sessions'])
        response = staff_client.get(SETTINGS_URL)
        assert response.context['excluded_apps'] == ['auth', 'sessions']

    def test_context_exclude_models_json(self, staff_client, staff_user):
        DiagramPreferences.objects.create(user=staff_user, exclude_models=[r'^Audit'])
        response = staff_client.get(SETTINGS_URL)
        assert r'^Audit' in response.context['exclude_models']

    def test_context_show_only_managed(self, staff_client, staff_user):
        DiagramPreferences.objects.create(user=staff_user, show_only_managed=True)
        response = staff_client.get(SETTINGS_URL)
        assert response.context['show_only_managed'] is True

    def test_context_show_inheritance(self, staff_client, staff_user):
        DiagramPreferences.objects.create(user=staff_user, show_inheritance=True)
        response = staff_client.get(SETTINGS_URL)
        assert response.context['show_inheritance'] is True

    # POST

    def test_post_saves_excluded_apps(self, staff_client, staff_user):
        response = staff_client.post(SETTINGS_URL, data={'excluded_apps': ['auth', 'sessions'], 'exclude_models': '[]'})
        assert response.status_code == 302
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert 'auth' in prefs.excluded_apps
        assert 'sessions' in prefs.excluded_apps

    def test_post_rejects_unknown_apps(self, staff_client, staff_user):
        staff_client.post(SETTINGS_URL, data={'excluded_apps': ['auth', 'nonexistent_app'], 'exclude_models': '[]'})
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert 'nonexistent_app' not in prefs.excluded_apps
        assert 'auth' in prefs.excluded_apps

    def test_post_redirects_to_diagram(self, staff_client):
        response = staff_client.post(SETTINGS_URL, data={'exclude_models': '[]'})
        assert response.status_code == 302
        assert response['Location'] == DIAGRAM_URL

    def test_post_no_apps_clears_excluded_apps(self, staff_client, staff_user):
        DiagramPreferences.objects.create(user=staff_user, excluded_apps=['auth'])
        staff_client.post(SETTINGS_URL, data={'exclude_models': '[]'})
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.excluded_apps == []

    def test_post_saves_exclude_models(self, staff_client, staff_user):
        staff_client.post(SETTINGS_URL, data={'exclude_models': json.dumps([r'^Historical', r'Log$'])})
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.exclude_models == [r'^Historical', r'Log$']

    def test_post_saves_show_only_managed_on(self, staff_client, staff_user):
        staff_client.post(SETTINGS_URL, data={'exclude_models': '[]', 'show_only_managed': 'on'})
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.show_only_managed is True

    def test_post_saves_show_only_managed_off(self, staff_client, staff_user):
        DiagramPreferences.objects.create(user=staff_user, show_only_managed=True)
        staff_client.post(SETTINGS_URL, data={'exclude_models': '[]'})
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.show_only_managed is False

    def test_post_saves_show_inheritance_on(self, staff_client, staff_user):
        staff_client.post(SETTINGS_URL, data={'exclude_models': '[]', 'show_inheritance': 'on'})
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.show_inheritance is True

    def test_post_saves_show_inheritance_off(self, staff_client, staff_user):
        DiagramPreferences.objects.create(user=staff_user, show_inheritance=True)
        staff_client.post(SETTINGS_URL, data={'exclude_models': '[]'})
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.show_inheritance is False

    def test_post_invalid_json_stores_empty_patterns(self, staff_client, staff_user):
        staff_client.post(SETTINGS_URL, data={'exclude_models': 'not-json'})
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.exclude_models == []

    def test_post_non_list_json_stores_empty_patterns(self, staff_client, staff_user):
        staff_client.post(SETTINGS_URL, data={'exclude_models': '"a string"'})
        prefs = DiagramPreferences.objects.get(user=staff_user)
        assert prefs.exclude_models == []
