"""
system_logs/tests/test_system_logs.py
The System Log: log_event, secret scrubbing, payment-error alerts (throttled),
the logging handler, M-Pesa instrumentation, and retention pruning.
"""

import logging
from datetime import timedelta
from io import StringIO
from unittest.mock import MagicMock, patch

from django.core import mail
from django.core.management import call_command
from django.test import TestCase, override_settings
from django.utils import timezone

from store.services.mpesa_service import MpesaError, MpesaService
from system_logs.models import SystemLog
from system_logs.services import log_event

MPESA_SETTINGS = dict(
    MOCK_MPESA=False,
    MPESA_STK_ENABLED=True,
    MPESA_SANDBOX=True,
    MPESA_CONSUMER_KEY="key",
    MPESA_CONSUMER_SECRET="secret",
    MPESA_SHORTCODE="174379",
    MPESA_PASSKEY="passkey",
    MPESA_CALLBACK_URL="https://example.test/api/payments/mpesa/callback/",
    SYSTEM_LOG_ASYNC_ALERTS=False,
    SYSTEM_ALERT_EMAILS=["alerts@example.test"],
    EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
)

CALLBACK_URL = "/api/payments/mpesa/callback/"


def paid_callback(checkout_id, receipt="UNMATCH01"):
    return {"Body": {"stkCallback": {
        "MerchantRequestID": "m-1", "CheckoutRequestID": checkout_id, "ResultCode": 0,
        "ResultDesc": "The service request is processed successfully.",
        "CallbackMetadata": {"Item": [
            {"Name": "Amount", "Value": 1},
            {"Name": "MpesaReceiptNumber", "Value": receipt},
            {"Name": "PhoneNumber", "Value": 254713621589},
        ]},
    }}}


@override_settings(**MPESA_SETTINGS)
class LogEventTests(TestCase):
    def test_writes_entry_and_scrubs_secrets(self):
        entry = log_event(
            SystemLog.MODULE_PAYMENTS, "test.event", "hello",
            request_data={"Password": "pw", "Amount": 1, "nested": {"access_token": "t"}},
        )
        entry.refresh_from_db()
        self.assertEqual(entry.request_data["Password"], "[removed]")
        self.assertEqual(entry.request_data["nested"]["access_token"], "[removed]")
        self.assertEqual(entry.request_data["Amount"], 1)

    def test_records_traceback(self):
        try:
            raise ValueError("boom")
        except ValueError as exc:
            entry = log_event(SystemLog.MODULE_SALES, "test.crash", "it broke",
                              level=SystemLog.LEVEL_ERROR, exc=exc)
        self.assertIn("ValueError: boom", entry.traceback)

    def test_alert_email_sent_once_per_reference(self):
        for _ in range(2):
            log_event(SystemLog.MODULE_PAYMENTS, "test.alert", "money problem",
                      level=SystemLog.LEVEL_ERROR, mpesa_receipt="R1", alert=True)
        log_event(SystemLog.MODULE_PAYMENTS, "test.alert", "different payment",
                  level=SystemLog.LEVEL_ERROR, mpesa_receipt="R2", alert=True)
        self.assertEqual(len(mail.outbox), 2)
        self.assertIn("alerts@example.test", mail.outbox[0].to)
        self.assertIn("/admin/system_logs/systemlog/", mail.outbox[0].body)

    def test_no_alert_for_warnings(self):
        log_event(SystemLog.MODULE_PAYMENTS, "test.warn", "meh", level=SystemLog.LEVEL_WARNING, alert=True)
        self.assertEqual(len(mail.outbox), 0)

    def test_logger_errors_reach_system_log(self):
        logging.getLogger("inventory.services.something").error("stock went sideways")
        entry = SystemLog.objects.get(message="stock went sideways")
        self.assertEqual(entry.module, SystemLog.MODULE_INVENTORY)
        self.assertEqual(entry.level, SystemLog.LEVEL_ERROR)


@override_settings(**MPESA_SETTINGS)
class PaymentInstrumentationTests(TestCase):
    def test_unmatched_paid_callback_is_logged_and_alerted(self):
        self.client.post(CALLBACK_URL, paid_callback("ws_NOPE"), content_type="application/json")
        received = SystemLog.objects.get(event="mpesa.callback.received")
        self.assertEqual(received.mpesa_receipt, "UNMATCH01")
        self.assertEqual(received.response_data["Body"]["stkCallback"]["CheckoutRequestID"], "ws_NOPE")
        unmatched = SystemLog.objects.get(event="mpesa.callback.unmatched")
        self.assertEqual(unmatched.level, SystemLog.LEVEL_ERROR)
        self.assertEqual(unmatched.phone_number, "254713621589")
        self.assertEqual(len(mail.outbox), 1)

    def test_garbage_callback_is_kept(self):
        resp = self.client.post(CALLBACK_URL, {"unexpected": True}, content_type="application/json")
        self.assertEqual(resp.status_code, 200)
        self.assertTrue(SystemLog.objects.filter(event="mpesa.callback.received", response_data__unexpected=True).exists())
        self.assertTrue(SystemLog.objects.filter(event="mpesa.callback.invalid").exists())

    def test_crash_while_processing_callback_is_logged(self):
        with patch("store.views.payment_service.find_order_for_checkout", side_effect=RuntimeError("db down")):
            resp = self.client.post(CALLBACK_URL, paid_callback("ws_X"), content_type="application/json")
        self.assertEqual(resp.status_code, 200)
        crashed = SystemLog.objects.get(event="mpesa.callback.crashed")
        self.assertIn("RuntimeError: db down", crashed.traceback)

    def test_stk_push_http_error_records_response_without_password(self):
        response = MagicMock(status_code=500, text='{"errorMessage": "Bad Request - Invalid PhoneNumber"}')
        response.json.return_value = {"errorMessage": "Bad Request - Invalid PhoneNumber"}
        response.raise_for_status.side_effect = __import__("requests").HTTPError(response=response)
        with patch.object(MpesaService, "_get_access_token", return_value="tok"), \
             patch("store.services.mpesa_service.requests.post", return_value=response):
            with self.assertRaises(MpesaError):
                MpesaService().stk_push("254713621589", 1500, "ORD-TEST")
        entry = SystemLog.objects.get(event="mpesa.stk_push.failed")
        self.assertEqual(entry.http_status, 500)
        self.assertEqual(entry.response_data["errorMessage"], "Bad Request - Invalid PhoneNumber")
        self.assertEqual(entry.request_data["Password"], "[removed]")
        self.assertEqual(entry.reference, "ORD-TEST")


@override_settings(**MPESA_SETTINGS)
class ServiceInstrumentationTests(TestCase):
    def test_refused_checkout_is_logged_with_phone(self):
        resp = self.client.post("/api/checkout/", {
            "customer_name": "Test", "phone_number": "0713621589",
            "delivery_type": "pickup", "items": [],
        }, content_type="application/json")
        self.assertGreaterEqual(resp.status_code, 400)
        entry = SystemLog.objects.get(event="store.checkout.refused")
        self.assertEqual(entry.phone_number, "0713621589")
        self.assertEqual(entry.http_status, resp.status_code)

    def test_failed_receipt_email_reaches_system_log(self):
        from inventory.services import receipt_service

        sale = MagicMock(invoice_number="INV-TEST", customer_email="c@example.test")
        with patch.object(receipt_service, "render_to_string", return_value="<p>receipt</p>"), \
             patch.object(receipt_service, "send_mail", side_effect=ConnectionError("SMTP down")):
            self.assertFalse(receipt_service.send_receipt_email(sale))
        entry = SystemLog.objects.get(module=SystemLog.MODULE_INVENTORY, level=SystemLog.LEVEL_ERROR)
        self.assertIn("INV-TEST", entry.message)
        self.assertIn("SMTP down", entry.traceback)


@override_settings(**MPESA_SETTINGS)
class PruneTests(TestCase):
    def test_prunes_entries_past_retention(self):
        old = log_event(SystemLog.MODULE_SYSTEM, "old", "old")
        new = log_event(SystemLog.MODULE_SYSTEM, "new", "new")
        SystemLog.objects.filter(pk=old.pk).update(created_at=timezone.now() - timedelta(days=200))
        call_command("prune_system_logs", "--days", "183", stdout=StringIO())
        self.assertFalse(SystemLog.objects.filter(pk=old.pk).exists())
        self.assertTrue(SystemLog.objects.filter(pk=new.pk).exists())
