import datetime
from unittest import mock
from zoneinfo import ZoneInfo

from django.test import SimpleTestCase, TestCase, override_settings
from rest_framework.test import APIClient

from accounts.models import Role, User
from email_reports import services
from email_reports.models import EmailSettings, ScheduledReportLog, ScheduledReportRecipient

NAIROBI = ZoneInfo('Africa/Nairobi')


def at(*args):
    return datetime.datetime(*args, tzinfo=NAIROBI)


class FakeResend:
    def __init__(self, result=None, error=None):
        self.Emails = mock.Mock()
        if error:
            self.Emails.send.side_effect = error
        else:
            self.Emails.send.return_value = result if result is not None else {'id': 'msg_1'}


class HelperTests(SimpleTestCase):
    def test_html_to_text_strips_tags(self):
        self.assertEqual(services.html_to_text('<h2>Title</h2><p>A &amp; B</p>'), 'Title\nA & B')

    def test_escape_helper(self):
        self.assertEqual(services._e('<script>"x"</script>'), '&lt;script&gt;&quot;x&quot;&lt;/script&gt;')


class SendViaResendTests(TestCase):
    @override_settings(RESEND_API_KEY='', RESEND_FROM_EMAIL='')
    def test_missing_config_is_reported(self):
        with self.assertRaisesMessage(services.EmailSendError, 'not configured'):
            services.send_via_resend('a@b.co', 's', '<p>x</p>')

    @override_settings(RESEND_API_KEY='re_env', RESEND_FROM_EMAIL='Env <env@shop.co>')
    def test_environment_is_the_fallback(self):
        fake = FakeResend()
        with mock.patch.object(services, 'resend', fake):
            self.assertEqual(services.send_via_resend('a@b.co', 'Hi', '<p>Hello <b>you</b></p>'), 'msg_1')
        payload = fake.Emails.send.call_args[0][0]
        self.assertEqual(payload['from'], 'Env <env@shop.co>')
        self.assertEqual(payload['to'], ['a@b.co'])
        self.assertIn('Hello you', payload['text'])
        self.assertEqual(fake.api_key, 're_env')

    @override_settings(RESEND_API_KEY='re_env', RESEND_FROM_EMAIL='Env <env@shop.co>')
    def test_saved_settings_override_environment(self):
        EmailSettings.objects.update_or_create(pk=1, defaults={'resend_api_key': 're_db', 'from_email': 'Db <db@shop.co>'})
        fake = FakeResend()
        with mock.patch.object(services, 'resend', fake):
            services.send_via_resend('a@b.co', 'Hi', '<p>x</p>')
        self.assertEqual(fake.api_key, 're_db')
        self.assertEqual(fake.Emails.send.call_args[0][0]['from'], 'Db <db@shop.co>')

    @override_settings(RESEND_API_KEY='re_env', RESEND_FROM_EMAIL='a <a@shop.co>')
    def test_api_error_and_missing_id_become_email_send_error(self):
        with mock.patch.object(services, 'resend', FakeResend(error=RuntimeError('domain not verified'))):
            with self.assertRaisesMessage(services.EmailSendError, 'domain not verified'):
                services.send_via_resend('a@b.co', 's', '<p>x</p>')
        with mock.patch.object(services, 'resend', FakeResend(result={})):
            with self.assertRaises(services.EmailSendError):
                services.send_via_resend('a@b.co', 's', '<p>x</p>')

    def test_summary_email_labels_period_and_has_no_raw_script(self):
        subject, html = services.compose_summary_email('this_week')
        self.assertIn('Weekly Summary', subject)
        self.assertNotIn('<script', html)


class ScheduleTests(TestCase):
    def setUp(self):
        self.r = ScheduledReportRecipient.objects.create(email='a@b.co', frequency='DAILY', send_hour=7, send_minute=30)

    def test_time_must_be_reached_including_minutes(self):
        self.assertFalse(services.is_due(self.r, at(2026, 10, 5, 7, 29)))
        self.assertTrue(services.is_due(self.r, at(2026, 10, 5, 7, 30)))

    def test_late_cron_run_still_sends_once(self):
        self.assertTrue(services.is_due(self.r, at(2026, 10, 5, 15, 0)))
        ScheduledReportLog.objects.create(recipient=self.r, success=True)
        ScheduledReportLog.objects.filter(recipient=self.r).update(sent_at=at(2026, 10, 5, 15, 0))
        self.assertFalse(services.is_due(self.r, at(2026, 10, 5, 16, 0)))

    def test_failed_send_is_retried_on_next_run(self):
        ScheduledReportLog.objects.create(recipient=self.r, success=False)
        ScheduledReportLog.objects.filter(recipient=self.r).update(sent_at=at(2026, 10, 5, 8, 0))
        self.assertTrue(services.is_due(self.r, at(2026, 10, 5, 9, 0)))

    def test_weekly_monthly_and_disabled(self):
        self.r.frequency, self.r.weekly_weekday = 'WEEKLY', 0  # 2026-10-05 is a Monday
        self.assertTrue(services.is_due(self.r, at(2026, 10, 5, 8)))
        self.assertFalse(services.is_due(self.r, at(2026, 10, 6, 8)))
        self.r.frequency = 'MONTHLY'
        self.assertTrue(services.is_due(self.r, at(2026, 11, 1, 8)))
        self.assertFalse(services.is_due(self.r, at(2026, 11, 2, 8)))
        self.r.enabled = False
        self.assertFalse(services.is_due(self.r, at(2026, 11, 1, 8)))

    def test_invalid_saved_zone_falls_back_to_utc(self):
        self.assertEqual(services.get_zone('Not/AZone').key, 'UTC')


class ApiTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(username='boss', password='pw', role=Role.objects.get(name='Manager'))
        self.client = APIClient()
        self.client.force_authenticate(self.user)

    def test_settings_roundtrip_never_returns_the_key(self):
        res = self.client.put('/api/email-settings/', {
            'resend_api_key': 're_abcdefghijkl', 'from_email': 'Shop <reports@shop.co>', 'time_zone': 'Africa/Dar_es_Salaam',
        }, format='json')
        self.assertEqual(res.status_code, 200)
        body = res.json()
        self.assertTrue(body['api_key_set'])
        self.assertNotIn('re_abcdefghijkl', str(body))
        self.assertEqual(body['time_zone'], 'Africa/Dar_es_Salaam')
        self.assertEqual(EmailSettings.load().resend_api_key, 're_abcdefghijkl')
        # blank key keeps the saved one; clear_api_key removes it
        self.client.put('/api/email-settings/', {'resend_api_key': '', 'from_email': 'reports@shop.co'}, format='json')
        self.assertEqual(EmailSettings.load().resend_api_key, 're_abcdefghijkl')
        self.client.put('/api/email-settings/', {'clear_api_key': True}, format='json')
        self.assertEqual(EmailSettings.load().resend_api_key, '')

    def test_settings_validation(self):
        self.assertEqual(self.client.put('/api/email-settings/', {'time_zone': 'Mars/Base'}, format='json').status_code, 400)
        self.assertEqual(self.client.put('/api/email-settings/', {'from_email': 'not an email'}, format='json').status_code, 400)

    def test_requires_permission(self):
        seller = User.objects.create_user(username='clerk', password='pw', role=Role.objects.get(name='Seller'))
        c = APIClient()
        c.force_authenticate(seller)
        self.assertEqual(c.get('/api/email-settings/').status_code, 403)

    def test_bulk_add_dedupes_and_reports_invalid(self):
        ScheduledReportRecipient.objects.create(email='old@shop.co', frequency='DAILY', send_hour=7)
        res = self.client.post('/api/scheduled-report-recipients/bulk/', {
            'emails': 'New@Shop.co, old@shop.co; bad-email\nsecond@shop.co new@shop.co',
            'frequency': 'DAILY', 'send_hour': 18, 'send_minute': 45,
        }, format='json')
        self.assertEqual(res.status_code, 201)
        body = res.json()
        self.assertEqual(sorted(r['email'] for r in body['created']), ['new@shop.co', 'second@shop.co'])
        self.assertEqual(body['skipped'], ['old@shop.co'])
        self.assertEqual(body['invalid'], ['bad-email'])
        self.assertEqual(ScheduledReportRecipient.objects.get(email='new@shop.co').send_minute, 45)

    @override_settings(RESEND_API_KEY='re_x', RESEND_FROM_EMAIL='S <s@shop.co>')
    def test_test_endpoint_reports_partial_failure(self):
        fake = FakeResend()
        fake.Emails.send.side_effect = [{'id': '1'}, RuntimeError('rejected')]
        with mock.patch.object(services, 'resend', fake):
            res = self.client.post('/api/email-settings/test/', {'emails': ['a@b.co', 'c@d.co']}, format='json')
        self.assertEqual(res.status_code, 200)
        self.assertEqual(res.json()['sent'], ['a@b.co'])
        self.assertEqual(res.json()['failed'][0]['email'], 'c@d.co')

    def test_test_endpoint_rejects_bad_input(self):
        self.assertEqual(self.client.post('/api/email-settings/test/', {'emails': ''}, format='json').status_code, 400)
        self.assertEqual(self.client.post('/api/email-settings/test/', {'emails': 'nope'}, format='json').status_code, 400)
