"""Tests for django_devicehub.auth — DeviceAuthBackend and broker views."""

import json

import pytest
from django.test import Client

from django_devicehub.auth.backends import DeviceAuthBackend

from tests.testapp.models import (
    WeatherStationDevice,
    SoilSensorDevice,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def auth_device(db):
    """A WeatherStationDevice with credentials already generated and saved."""
    device = WeatherStationDevice.objects.create(
        device_id="auth-ws-001",
        name="Auth Test Station",
    )
    creds = device.generate_credentials()
    device.save()
    return device, creds


@pytest.fixture
def auth_soil_device(db):
    """A SoilSensorDevice with credentials."""
    device = SoilSensorDevice.objects.create(
        device_id="auth-ss-001",
        name="Auth Soil Sensor",
    )
    creds = device.generate_credentials()
    device.save()
    return device, creds


@pytest.fixture
def client():
    return Client()


# ---------------------------------------------------------------------------
# DeviceAuthBackend.authenticate()
# ---------------------------------------------------------------------------


class TestDeviceAuthBackend:
    @pytest.mark.django_db
    def test_valid_credentials_returns_device(self, auth_device):
        device, creds = auth_device
        result = DeviceAuthBackend.authenticate(
            creds["mqtt_username"],
            creds["mqtt_password"],
        )
        assert result is not None
        assert result.pk == device.pk
        assert result.device_id == "auth-ws-001"

    @pytest.mark.django_db
    def test_wrong_password_returns_none(self, auth_device):
        _, creds = auth_device
        result = DeviceAuthBackend.authenticate(
            creds["mqtt_username"],
            "bad-password",
        )
        assert result is None

    @pytest.mark.django_db
    def test_unknown_username_returns_none(self, auth_device):
        result = DeviceAuthBackend.authenticate("nobody", "password")
        assert result is None

    @pytest.mark.django_db
    def test_inactive_device_returns_none(self, auth_device):
        device, creds = auth_device
        device.is_active = False
        device.save()
        result = DeviceAuthBackend.authenticate(
            creds["mqtt_username"],
            creds["mqtt_password"],
        )
        assert result is None

    @pytest.mark.django_db
    def test_authenticates_across_device_types(self, auth_device, auth_soil_device):
        """Backend searches all registered models; soil sensor creds should work."""
        _, soil_creds = auth_soil_device
        result = DeviceAuthBackend.authenticate(
            soil_creds["mqtt_username"],
            soil_creds["mqtt_password"],
        )
        assert result is not None
        assert result.device_id == "auth-ss-001"


# ---------------------------------------------------------------------------
# DeviceAuthBackend.authenticate_api_key()
# ---------------------------------------------------------------------------


class TestAuthenticateApiKey:
    @pytest.mark.django_db
    def test_valid_api_key(self, auth_device):
        device, creds = auth_device
        result = DeviceAuthBackend.authenticate_api_key(creds["api_key"])
        assert result is not None
        assert result.pk == device.pk

    @pytest.mark.django_db
    def test_invalid_api_key(self, auth_device):
        result = DeviceAuthBackend.authenticate_api_key("not-a-real-key")
        assert result is None

    @pytest.mark.django_db
    def test_inactive_device_api_key(self, auth_device):
        device, creds = auth_device
        device.is_active = False
        device.save()
        result = DeviceAuthBackend.authenticate_api_key(creds["api_key"])
        assert result is None

    @pytest.mark.django_db
    def test_api_key_across_device_types(self, auth_soil_device):
        _, creds = auth_soil_device
        result = DeviceAuthBackend.authenticate_api_key(creds["api_key"])
        assert result is not None
        assert result.device_id == "auth-ss-001"


# ---------------------------------------------------------------------------
# BrokerAuthView
# ---------------------------------------------------------------------------


class TestBrokerAuthView:
    @pytest.mark.django_db
    def test_valid_credentials_returns_allow(self, client, auth_device):
        _, creds = auth_device
        response = client.post(
            "/iot/broker/auth/",
            data=json.dumps(
                {
                    "username": creds["mqtt_username"],
                    "password": creds["mqtt_password"],
                }
            ),
            content_type="application/json",
        )
        assert response.status_code == 200
        body = response.json()
        assert body["result"] == "allow"

    @pytest.mark.django_db
    def test_invalid_credentials_returns_deny(self, client, auth_device):
        _, creds = auth_device
        response = client.post(
            "/iot/broker/auth/",
            data=json.dumps(
                {
                    "username": creds["mqtt_username"],
                    "password": "wrong",
                }
            ),
            content_type="application/json",
        )
        assert response.status_code == 200
        body = response.json()
        assert body["result"] == "deny"

    @pytest.mark.django_db
    def test_unknown_username_returns_deny(self, client):
        response = client.post(
            "/iot/broker/auth/",
            data=json.dumps(
                {
                    "username": "ghost",
                    "password": "none",
                }
            ),
            content_type="application/json",
        )
        assert response.status_code == 200
        assert response.json()["result"] == "deny"

    @pytest.mark.django_db
    def test_empty_body_returns_deny(self, client):
        response = client.post(
            "/iot/broker/auth/",
            data=json.dumps({}),
            content_type="application/json",
        )
        assert response.json()["result"] == "deny"

    @pytest.mark.django_db
    def test_malformed_json_returns_deny(self, client):
        response = client.post(
            "/iot/broker/auth/",
            data="not json",
            content_type="application/json",
        )
        assert response.status_code == 400
        assert response.json()["result"] == "deny"

    @pytest.mark.django_db
    def test_auth_marks_device_online(self, client, auth_device):
        device, creds = auth_device
        response = client.post(
            "/iot/broker/auth/",
            data=json.dumps(
                {
                    "username": creds["mqtt_username"],
                    "password": creds["mqtt_password"],
                }
            ),
            content_type="application/json",
        )
        assert response.json()["result"] == "allow"
        device.refresh_from_db()
        assert device.status == "online"


# ---------------------------------------------------------------------------
# BrokerACLView
# ---------------------------------------------------------------------------


class TestBrokerACLView:
    @pytest.mark.django_db
    def test_device_can_publish_to_own_data_topic(self, client, auth_device):
        device, creds = auth_device
        response = client.post(
            "/iot/broker/acl/",
            data=json.dumps(
                {
                    "username": creds["mqtt_username"],
                    "topic": "test/weatherstation/auth-ws-001/data",
                    "action": "pub",
                }
            ),
            content_type="application/json",
        )
        assert response.json()["result"] == "allow"

    @pytest.mark.django_db
    def test_device_can_publish_to_own_status_topic(self, client, auth_device):
        device, creds = auth_device
        response = client.post(
            "/iot/broker/acl/",
            data=json.dumps(
                {
                    "username": creds["mqtt_username"],
                    "topic": "test/weatherstation/auth-ws-001/status",
                    "action": "pub",
                }
            ),
            content_type="application/json",
        )
        assert response.json()["result"] == "allow"

    @pytest.mark.django_db
    def test_device_cannot_publish_to_other_device_topic(self, client, auth_device):
        _, creds = auth_device
        response = client.post(
            "/iot/broker/acl/",
            data=json.dumps(
                {
                    "username": creds["mqtt_username"],
                    "topic": "test/weatherstation/other-device/data",
                    "action": "pub",
                }
            ),
            content_type="application/json",
        )
        assert response.json()["result"] == "deny"

    @pytest.mark.django_db
    def test_device_cannot_publish_to_different_type_topic(self, client, auth_device):
        _, creds = auth_device
        response = client.post(
            "/iot/broker/acl/",
            data=json.dumps(
                {
                    "username": creds["mqtt_username"],
                    "topic": "test/soilsensor/auth-ws-001/data",
                    "action": "pub",
                }
            ),
            content_type="application/json",
        )
        assert response.json()["result"] == "deny"

    @pytest.mark.django_db
    def test_device_can_subscribe_to_own_cmd_topic(self, client, auth_device):
        _, creds = auth_device
        response = client.post(
            "/iot/broker/acl/",
            data=json.dumps(
                {
                    "username": creds["mqtt_username"],
                    "topic": "test/weatherstation/auth-ws-001/cmd",
                    "action": "sub",
                }
            ),
            content_type="application/json",
        )
        assert response.json()["result"] == "allow"

    @pytest.mark.django_db
    def test_device_cannot_publish_to_cmd_topic(self, client, auth_device):
        """Devices should not be able to publish to their own cmd topic."""
        _, creds = auth_device
        response = client.post(
            "/iot/broker/acl/",
            data=json.dumps(
                {
                    "username": creds["mqtt_username"],
                    "topic": "test/weatherstation/auth-ws-001/cmd",
                    "action": "pub",
                }
            ),
            content_type="application/json",
        )
        assert response.json()["result"] == "deny"

    @pytest.mark.django_db
    def test_unknown_username_denied(self, client):
        response = client.post(
            "/iot/broker/acl/",
            data=json.dumps(
                {
                    "username": "ghost",
                    "topic": "test/weatherstation/ws-001/data",
                    "action": "pub",
                }
            ),
            content_type="application/json",
        )
        assert response.json()["result"] == "deny"

    @pytest.mark.django_db
    def test_empty_body_denied(self, client):
        response = client.post(
            "/iot/broker/acl/",
            data=json.dumps({}),
            content_type="application/json",
        )
        assert response.json()["result"] == "deny"

    @pytest.mark.django_db
    def test_malformed_json_denied(self, client):
        response = client.post(
            "/iot/broker/acl/",
            data="{{bad",
            content_type="application/json",
        )
        assert response.status_code == 400
        assert response.json()["result"] == "deny"
