"""
SEO / AEO checks. Run:  venv/bin/python manage.py test apps.core
"""
import json
import re

from django.conf import settings
from django.test import TestCase, override_settings

from apps.articles.models import Article, Author, Category


@override_settings(ALLOWED_HOSTS=['testserver', 'thereversetimes.com', 'www.thereversetimes.com'])
class SeoTests(TestCase):
    def setUp(self):
        self.cat = Category.objects.create(name='Mughal Bengal', slug='mughal-bengal')
        self.author = Author.objects.create(name='Rahim Uddin', slug='rahim-uddin')
        self.article = Article.objects.create(
            title='Lalbagh Fort: an unfinished dream', slug='lalbagh-fort', author=self.author,
            status='published', excerpt='Why the fort was never finished.',
            body='<p>' + 'word ' * 450 + '</p>', featured_image='articles/x.jpg',
            historical_period='Mughal Bengal')
        self.article.categories.add(self.cat)
        from django.utils import timezone
        Article.objects.filter(pk=self.article.pk).update(published_at=timezone.now())

    def html(self, url):
        r = self.client.get(url)
        self.assertEqual(r.status_code, 200, url)
        return r.content.decode()

    def jsonld(self, html):
        return [json.loads(b) for b in re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.S)]

    def test_sitemap_urls_are_clean(self):
        body = self.html('/sitemap.xml')
        self.assertIn('<loc>https://thereversetimes.com/</loc>', body)
        self.assertIn('<loc>https://thereversetimes.com/articles/lalbagh-fort/</loc>', body)
        self.assertNotIn('https://https', body)
        self.assertNotIn('.com//', body)

    def test_www_redirects_to_main_domain(self):
        r = self.client.get('/about/?a=1', HTTP_HOST='www.thereversetimes.com')
        self.assertEqual(r.status_code, 301)
        self.assertEqual(r['Location'], 'https://thereversetimes.com/about/?a=1')

    def test_homepage_brand_h1_and_canonical(self):
        html = self.html('/')
        self.assertEqual(len(re.findall(r'<h1[\s>]', html)), 1)
        self.assertIn("Bangladesh's First Historical Magazine", re.search(r'<h1[^>]*>(.*?)</h1>', html, re.S).group(1))
        self.assertIn('<link rel="canonical" href="https://thereversetimes.com/">', html)
        self.assertNotIn('images.unsplash.com', html)  # no placeholder content
        graph = self.jsonld(html)[0]['@graph']
        self.assertEqual(graph[0]['name'], 'The Reverse Times')
        self.assertTrue(graph[0]['logo']['url'].endswith('.png'))

    def test_articles_list_points_to_homepage(self):
        self.assertIn('<link rel="canonical" href="https://thereversetimes.com/">', self.html('/articles/'))

    def test_article_schema_and_reading_time(self):
        html = self.html('/articles/lalbagh-fort/')
        article = next(n for b in self.jsonld(html) if '@graph' in b for n in b['@graph'] if n['@type'] == 'NewsArticle')
        self.assertEqual(article['author'][0]['name'], 'Rahim Uddin')
        self.assertEqual(article['timeRequired'], 'PT2M')
        self.assertIn('2 min read', html)
        self.assertIn('<link rel="canonical" href="https://thereversetimes.com/articles/lalbagh-fort/">', html)

    def test_today_exclusive_canonical_is_the_article(self):
        html = self.html('/articles/exclusive/today/')
        self.assertIn('<link rel="canonical" href="https://thereversetimes.com/articles/lalbagh-fort/">', html)

    def test_search_is_noindex(self):
        self.assertIn('content="noindex, follow"', self.html('/search/?q=fort'))

    def test_ai_files(self):
        robots = self.html('/robots.txt')
        self.assertIn('User-agent: GPTBot', robots)
        self.assertIn('User-agent: ClaudeBot', robots)
        self.assertIn('Sitemap: https://thereversetimes.com/sitemap.xml', robots)
        llms = self.html('/llms.txt')
        self.assertIn('Lalbagh Fort: an unfinished dream', llms)
        self.assertIn('Reverse the Time', llms)
        self.assertIn('Lalbagh Fort', self.html('/feed/'))
        self.assertEqual(self.html(f'/{settings.INDEXNOW_KEY}.txt'), settings.INDEXNOW_KEY)

    def test_about_faq_schema_is_valid(self):
        blocks = self.jsonld(self.html('/about/'))
        faq = next(b for b in blocks if b.get('@type') == 'FAQPage')
        names = [q['name'] for q in faq['mainEntity']]
        self.assertIn('Is The Reverse Times the same as "Reverse the Time"?', names)


@override_settings(ALLOWED_HOSTS=['testserver'])
class BanglaTagTests(TestCase):
    def test_bangla_tags_keep_spelling_and_pages_work(self):
        from django.utils import timezone
        a = Article.objects.create(title='T', slug='t', body='<p>x</p>', excerpt='x', status='published',
                                   featured_image='articles/x.jpg')
        Article.objects.filter(pk=a.pk).update(published_at=timezone.now())
        a.tags.add('হারিয়ে যাওয়া পেশা')
        from taggit.models import Tag
        self.assertEqual(Tag.objects.get().slug, 'হারিয়ে-যাওয়া-পেশা')
        self.assertEqual(self.client.get('/articles/t/').status_code, 200)
        self.assertEqual(self.client.get('/articles/tag/হারিয়ে-যাওয়া-পেশা/').status_code, 200)
        self.assertEqual(self.client.get('/sitemap.xml').status_code, 200)


@override_settings(ALLOWED_HOSTS=['testserver'])
class VisitorLogTests(TestCase):
    UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1'

    def beat(self, seconds=0, ua=UA, view='a' * 32, path='/articles/x/'):
        return self.client.post('/api/visit/', json.dumps({'v': view, 'u': 'b' * 32, 'p': path, 's': seconds}),
                                content_type='text/plain', HTTP_USER_AGENT=ua)

    def test_humans_logged_bots_and_page_loads_ignored(self):
        from apps.core.models import VisitorLog
        self.client.get('/')  # plain page loads no longer write logs or create sessions
        self.assertEqual(VisitorLog.objects.count(), 0)
        self.assertEqual(self.beat(ua='Mozilla/5.0 (compatible; Googlebot/2.1)').status_code, 204)
        self.assertEqual(self.beat(ua='facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)').status_code, 204)
        self.assertEqual(VisitorLog.objects.count(), 0)
        self.beat(0)
        log = VisitorLog.objects.get()
        self.assertEqual((log.is_human, log.device, log.page_visited), (True, 'mobile', '/articles/x/'))

    def test_time_cannot_outrun_the_clock(self):
        from apps.core.models import VisitorLog
        self.beat(0)
        self.beat(5000)  # claims 83 minutes right after opening
        self.assertLessEqual(VisitorLog.objects.get().time_spent_seconds, 20)
        self.assertEqual(self.beat(path='/admin/', view='c' * 32).status_code, 400)


@override_settings(ALLOWED_HOSTS=['testserver'])
class HeroSliderTests(TestCase):
    def setUp(self):
        from decimal import Decimal
        from django.core.files.uploadedfile import SimpleUploadedFile
        from django.utils import timezone
        from apps.store.models import Product
        gif = (b'GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\xff\xff\xff!\xf9\x04\x00\x00\x00\x00\x00,'
               b'\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;')
        self.a = Article.objects.create(title='Lalbagh slide', slug='lalbagh-slide', body='<p>x</p>', excerpt='Fort story',
                                        status='published', featured_image='articles/x.jpg')
        Article.objects.filter(pk=self.a.pk).update(published_at=timezone.now())
        self.tee = Product.objects.create(product_type='merch', title='Slide Tee', slug='slide-tee', description='Cotton',
                                          price=Decimal('550'), image=SimpleUploadedFile('t.gif', gif, content_type='image/gif'))

    def test_slides_replace_single_hero_and_respect_schedule(self):
        from datetime import timedelta
        from django.utils import timezone
        from apps.core.models import HeroSlide
        HeroSlide.objects.all().delete()
        self.assertNotIn('data-hs', self.client.get('/').content.decode())  # no slides → old hero
        HeroSlide.objects.create(kind='article', article=self.a, order=1)
        HeroSlide.objects.create(kind='product', product=self.tee, order=2)
        HeroSlide.objects.create(kind='custom', title='Old campaign', order=3, ends_at=timezone.now() - timedelta(days=1))
        html = self.client.get('/').content.decode()
        self.assertIn('data-hs', html)
        self.assertIn('Lalbagh slide', html)
        self.assertIn('Slide Tee', html)
        self.assertIn('৳৫৫০', html)
        self.assertNotIn('Old campaign', html)
        self.assertIn('hero-slider.js', html)
        self.assertEqual(len(re.findall(r'<h1[\s>]', html)), 1)

    def test_admin_pages(self):
        from django.contrib.auth.models import User
        User.objects.create_superuser('boss2', 'b2@example.com', 'pw-123456789')
        self.client.login(username='boss2', password='pw-123456789')
        for url in ('/admin/core/heroslide/', '/admin/core/heroslide/add/'):
            self.assertEqual(self.client.get(url).status_code, 200, url)


@override_settings(ALLOWED_HOSTS=['testserver'])
class HeroSlidePhonePictureTests(TestCase):
    def test_phone_picture_options(self):
        from decimal import Decimal
        from django.core.files.uploadedfile import SimpleUploadedFile
        from apps.core.models import HeroSlide
        from apps.store.models import Product
        gif = (b'GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\xff\xff\xff!\xf9\x04\x00\x00\x00\x00\x00,'
               b'\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;')
        tee = Product.objects.create(product_type='merch', title='Phone Tee', slug='phone-tee', description='x',
                                     price=Decimal('500'), image=SimpleUploadedFile('p.gif', gif, content_type='image/gif'))
        HeroSlide.objects.all().delete()
        s = HeroSlide.objects.create(kind='product', product=tee)
        d = s.display()
        self.assertTrue(d['phone_plain'])          # default: no custom picture → nothing on phones
        self.assertFalse(d['phone_side'])
        self.assertIn('phone-plain', self.client.get('/').content.decode())
        s.phone_display = 'product'
        s.save()
        d = s.display()
        self.assertTrue(d['phone_side'])
        self.assertFalse(d['phone_plain'])
        s.phone_display = 'custom'
        s.phone_image = SimpleUploadedFile('m.gif', gif, content_type='image/gif')
        s.save()
        html = self.client.get('/').content.decode()
        self.assertIn('media="(max-width: 768px)"', html)
        self.assertIn(s.phone_image.url, html)


@override_settings(ALLOWED_HOSTS=['testserver'])
class AnalyticsTests(TestCase):
    UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15'

    def ping(self, view, user='b' * 32, path='/', **extra):
        body = {'v': view, 'u': user, 'p': path, 's': 0, **extra}
        return self.client.post('/api/visit/', json.dumps(body), content_type='text/plain', HTTP_USER_AGENT=self.UA)

    def test_beacon_stores_scroll_title_campaign_and_clicks(self):
        from apps.core.models import ClickEvent, VisitorLog
        clicks = [{'l': '  Read   Feature ', 'h': '/articles/x/', 'k': 'link', 's': 'hero', 'x': 500, 'y': 2000},
                  {'l': 'Logo', 'k': 'weird'}]
        self.ping('a' * 32, t='Home', d=40, m={'source': 'facebook', 'campaign': 'eid'}, c=clicks)
        self.ping('a' * 32, d=25, c=[{'l': 'Shop', 'h': 'https://example.com/', 'k': 'outbound'}])
        log = VisitorLog.objects.get()
        self.assertEqual((log.page_title, log.scroll_pct, log.utm_source, log.utm_campaign), ('Home', 40, 'facebook', 'eid'))
        rows = list(ClickEvent.objects.order_by('id').values_list('label', 'kind', 'x', 'y', 'page'))
        self.assertEqual(rows[0], ('Read Feature', 'link', 500, 1000, '/'))  # y capped at 100%
        self.assertEqual(rows[1][1], 'other')
        self.assertEqual(rows[2][1], 'outbound')

    def test_clicks_capped_per_view(self):
        from apps.core import visits
        from apps.core.models import ClickEvent
        self.ping('a' * 32)
        for _ in range(12):
            self.ping('a' * 32, c=[{'l': 'x', 'k': 'button'}] * 40)
        self.assertEqual(ClickEvent.objects.count(), visits.MAX_CLICKS_PER_VIEW)

    def make(self, visitor, path, minutes, secs=30, ref='', scroll=50):
        from datetime import timedelta
        from django.utils import timezone
        from apps.core.models import VisitorLog
        at = timezone.now() - timedelta(hours=2) + timedelta(minutes=minutes)
        return VisitorLog.objects.create(visitor_id=visitor, is_human=True, page_visited=path, referrer=ref, device='mobile',
                                         visit_time=at, last_seen=at + timedelta(seconds=secs),
                                         time_spent_seconds=secs, scroll_pct=scroll)

    def test_sessions_entries_exits_and_flow(self):
        from datetime import timedelta
        from django.utils import timezone
        from apps.core import analytics
        # Visitor 1: home -> magazine (one visit), then back 50 minutes later on shop (second visit, bounce).
        self.make('1' * 32, '/', 0, ref='https://www.google.com/')
        self.make('1' * 32, '/magazine/', 1, secs=90)
        self.make('1' * 32, '/shop/', 52, ref='https://facebook.com/')
        # Visitor 2: home only.
        self.make('2' * 32, '/', 5, secs=10)
        now = timezone.now()
        a = analytics.build(now - timedelta(days=1), now, page='/')
        k = a['kpi']
        self.assertEqual((k['visitors'], k['sessions'], k['views']), (2, 3, 4))
        self.assertEqual(k['bounce_rate'], 66.7)
        pages = {r['path']: r for r in a['all_pages']}
        self.assertEqual((pages['/']['entries'], pages['/']['exits']), (2, 1))
        self.assertEqual(pages['/magazine/']['exit_rate'], 100.0)
        self.assertEqual(a['flows'][0]['from'], '/')
        self.assertEqual(a['flows'][0]['to'], '/magazine/')
        self.assertEqual({r['name'] for r in a['sources']}, {'Search', 'Social', 'Direct'})
        went = {r['name']: r['n'] for r in a['page_detail']['went']}
        self.assertEqual(went, {'/magazine/': 1, 'Left the website': 1})

    def test_source_classification(self):
        from apps.core.analytics import source_of
        self.assertEqual(source_of('https://www.google.com.bd/'), 'Search')
        self.assertEqual(source_of('https://l.facebook.com/'), 'Social')
        self.assertEqual(source_of('https://chatgpt.com/'), 'AI assistant')
        self.assertEqual(source_of('', 'newsletter'), 'Campaign')
        self.assertEqual(source_of(''), 'Direct')
        self.assertEqual(source_of('https://prothomalo.com/x'), 'Other website')

    def test_dashboard_staff_only_and_exports(self):
        from django.contrib.auth import get_user_model
        self.make('1' * 32, '/', 0)
        self.assertEqual(self.client.get('/admin/analytics/').status_code, 302)
        User = get_user_model()
        staff = User.objects.create_user(**{User.USERNAME_FIELD: 'boss@example.test'}, password='x-unused-1',
                                         is_staff=True, is_superuser=True)
        self.client.force_login(staff)
        for q in ('', '?range=today', '?range=90&device=mobile', '?range=custom&from=2026-01-01&to=2025-12-01',
                  '?range=bogus', '?page=/'):
            self.assertEqual(self.client.get('/admin/analytics/' + q).status_code, 200, q)
        r = self.client.get('/admin/analytics/?export=pages')
        self.assertEqual(r['Content-Type'], 'text/csv; charset=utf-8')
        self.assertIn('/,', r.content.decode('utf-8-sig'))


@override_settings(ALLOWED_HOSTS=['testserver', 'thereversetimes.com'])
class SiteNameTests(TestCase):
    """Google shows a site name above results; every signal must say "The Reverse Times", never the domain."""

    def test_homepage_site_name_signals(self):
        h = self.client.get('/', HTTP_HOST='thereversetimes.com', secure=True).content.decode()
        self.assertRegex(h, r'<title>The Reverse Times\b')
        self.assertIn('<meta property="og:site_name" content="The Reverse Times">', h)
        site = None
        for ld in re.findall(r'<script type="application/ld\+json">(.*?)</script>', h, re.S):
            data = json.loads(ld.replace('<\\/', '</'))
            for item in data.get('@graph', [data]):
                if item.get('@type') == 'WebSite':
                    site = item
        self.assertEqual(site['name'], 'The Reverse Times')
        self.assertEqual(site['url'], 'https://thereversetimes.com/')
        self.assertNotIn('thereversetimes.com', ' '.join(site.get('alternateName', [])))
