"""Rewarded-advertisement audit logging + analytics."""

import os
import unittest
from decimal import Decimal

os.environ["TEST_DATABASE_URL"] = "sqlite:///:memory:"

from werkzeug.security import generate_password_hash

from app import create_app
from app.admin.advertisements.services import rewarded_analytics, rewarded_ad_breakdown
from app.advertisements.rate_limit import reset_rate_limits
from app.advertisements.reward_token import issue_reward_token, reset_replay_guard
from app.chat.services import get_messaging_state
from app.extensions import db
from app.models import (
    AdminAuditLog,
    Advertisement,
    AdvertisementMediaType,
    AdvertisementPlacement,
    AdvertisementType,
    Category,
    Conversation,
    ConversationStatus,
    Listing,
    ListingCondition,
    ListingStatus,
    ListingVisibility,
    ReviewStatus,
    RewardedAdEvent,
    User,
)


class RewardObservabilityTests(unittest.TestCase):
    def setUp(self):
        self.app = create_app("testing")
        self.ctx = self.app.app_context()
        self.ctx.push()
        db.create_all()
        reset_rate_limits()
        reset_replay_guard()

    def tearDown(self):
        reset_rate_limits()
        reset_replay_guard()
        db.session.remove()
        db.drop_all()
        self.ctx.pop()

    # --- factories -------------------------------------------------------------
    def _user(self, email):
        u = User(email=email, password_hash=generate_password_hash("Passw0rd!"), is_verified=True)
        db.session.add(u)
        db.session.commit()
        return u

    def _convo(self, buyer, seller):
        cat = Category(name="Books", description="d", is_active=True, display_order=1)
        db.session.add(cat)
        db.session.commit()
        listing = Listing(
            sku=f"SKU-{seller.id}", seller_id=seller.id, category_id=cat.id, title="Book",
            slug=f"book-{seller.id}", description="d", price=Decimal("50"),
            condition=ListingCondition.GOOD, status=ListingStatus.AVAILABLE,
            visibility=ListingVisibility.PUBLIC, review_status=ReviewStatus.APPROVED,
        )
        db.session.add(listing)
        db.session.commit()
        convo = Conversation(listing_id=listing.id, buyer_id=buyer.id, seller_id=seller.id,
                             status=ConversationStatus.ACTIVE)
        db.session.add(convo)
        db.session.commit()
        return convo

    def _ad(self, *, video=False):
        kwargs = dict(title="Ad", advertisement_type=AdvertisementType.REWARDED,
                      placement=AdvertisementPlacement.CHAT, is_active=True, priority=10)
        if video:
            kwargs.update(media_type=AdvertisementMediaType.VIDEO,
                          media_url="https://cdn.example.com/ad.mp4", video_duration=15)
        else:
            kwargs.update(image_path="/x.png")
        ad = Advertisement(**kwargs)
        db.session.add(ad)
        db.session.commit()
        return ad

    def _login(self, email):
        c = self.app.test_client()
        c.post("/login", data={"email": email, "password": "Passw0rd!"})
        return c

    def _events(self, event_type, reason=None):
        q = db.select(db.func.count(RewardedAdEvent.id)).where(RewardedAdEvent.event_type == event_type)
        if reason is not None:
            q = q.where(RewardedAdEvent.reason == reason)
        return db.session.scalar(q)

    def _audit(self, action, reason=None):
        rows = db.session.scalars(db.select(AdminAuditLog).where(AdminAuditLog.action == action)).all()
        if reason is not None:
            rows = [r for r in rows if reason in (r.details or "")]
        return rows

    def _claim(self, client, convo, token, extra=None):
        data = {"reward_token": token}
        if extra:
            data.update(extra)
        return client.post(f"/app/chat/{convo.public_id}/watch-ad", data=data, follow_redirects=True)

    # ======================================================================== #
    # Analytics
    # ======================================================================== #
    def test_impression_counted_on_modal_open(self):
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad()
        self._login("b@c.edu").get(f"/app/chat/{convo.public_id}/reward-token?ad={ad.public_id}")
        self.assertEqual(self._events("impression"), 1)

    def test_video_events_counted(self):
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad(video=True)
        client = self._login("b@c.edu")
        for t in ("video_start", "video_success", "video_fallback", "video_error"):
            client.get(f"/app/chat/{convo.public_id}/ad-event?type={t}&ad={ad.public_id}")
        self.assertEqual(self._events("video_start"), 1)
        self.assertEqual(self._events("video_success"), 1)
        self.assertEqual(self._events("video_fallback"), 1)
        self.assertEqual(self._events("video_error"), 1)

    def test_reward_granted_counted(self):
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad()
        self._claim(self._login("b@c.edu"), convo, issue_reward_token(buyer.id, ad.public_id))
        self.assertEqual(self._events("reward_granted"), 1)

    def test_completion_rate_calculation(self):
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad()
        client = self._login("b@c.edu")
        # Two impressions (modal opened twice), one reward granted → 50%.
        client.get(f"/app/chat/{convo.public_id}/reward-token?ad={ad.public_id}")
        client.get(f"/app/chat/{convo.public_id}/reward-token?ad={ad.public_id}")
        self._claim(client, convo, issue_reward_token(buyer.id, ad.public_id))
        stats = rewarded_analytics()
        self.assertEqual(stats["impressions"], 2)
        self.assertEqual(stats["rewards_granted"], 1)
        self.assertEqual(stats["completion_rate"], 50.0)

    def test_rejections_counted_by_reason(self):
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad()
        client = self._login("b@c.edu")
        self._claim(client, convo, issue_reward_token(buyer.id, ad.public_id, ttl=-10))  # expired
        good = issue_reward_token(buyer.id, ad.public_id)
        self._claim(client, convo, good)          # granted
        self._claim(client, convo, good)          # replay
        self._claim(client, convo, "garbage")     # tampered/malformed
        self.assertEqual(self._events("reward_rejected", "expired"), 1)
        self.assertEqual(self._events("reward_rejected", "replay"), 1)
        self.assertEqual(self._events("reward_rejected", "tampered"), 1)
        stats = rewarded_analytics()
        self.assertEqual(stats["rejections"], 3)
        self.assertEqual(stats["rejections_by_reason"].get("expired"), 1)

    def test_breakdown_lists_ad_with_completion(self):
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad()
        client = self._login("b@c.edu")
        client.get(f"/app/chat/{convo.public_id}/reward-token?ad={ad.public_id}")
        self._claim(client, convo, issue_reward_token(buyer.id, ad.public_id))
        rows = rewarded_ad_breakdown()
        self.assertEqual(rows[0]["public_id"], ad.public_id)
        self.assertEqual(rows[0]["impressions"], 1)
        self.assertEqual(rows[0]["rewards"], 1)
        self.assertEqual(rows[0]["completion_rate"], 100.0)

    # ======================================================================== #
    # Audit logging (reuses AdminAuditLog)
    # ======================================================================== #
    def test_audit_reward_claimed(self):
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad()
        self._claim(self._login("b@c.edu"), convo, issue_reward_token(buyer.id, ad.public_id))
        rows = self._audit("reward_claimed")
        self.assertEqual(len(rows), 1)
        self.assertEqual(rows[0].admin_id, buyer.id)
        self.assertEqual(rows[0].entity_type, "advertisement")

    def test_audit_replay_and_expired_and_tampered(self):
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad()
        client = self._login("b@c.edu")
        good = issue_reward_token(buyer.id, ad.public_id)
        self._claim(client, convo, good)        # granted
        self._claim(client, convo, good)        # replay
        self._claim(client, convo, issue_reward_token(buyer.id, ad.public_id, ttl=-10))  # expired
        self._claim(client, convo, "garbage")   # tampered
        self.assertEqual(len(self._audit("reward_rejected", "replay")), 1)
        self.assertEqual(len(self._audit("reward_rejected", "expired")), 1)
        self.assertEqual(len(self._audit("reward_rejected", "tampered")), 1)

    def test_audit_inactive_advertisement(self):
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad()
        token = issue_reward_token(buyer.id, ad.public_id)
        ad.is_active = False
        db.session.commit()
        self._claim(self._login("b@c.edu"), convo, token)
        self.assertEqual(len(self._audit("reward_rejected", "inactive")), 1)

    def test_audit_rate_limited(self):
        self.app.config["REWARD_CLAIM_RATE_MAX"] = 1
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad()
        client = self._login("b@c.edu")
        self._claim(client, convo, issue_reward_token(buyer.id, ad.public_id))  # 1st → granted
        self._claim(client, convo, issue_reward_token(buyer.id, ad.public_id))  # 2nd → rate-limited
        self.assertEqual(len(self._audit("reward_rejected", "rate_limit")), 1)

    def test_audit_video_failed(self):
        buyer, seller = self._user("b@c.edu"), self._user("s@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._ad(video=True)
        self._login("b@c.edu").get(f"/app/chat/{convo.public_id}/ad-event?type=video_error&ad={ad.public_id}")
        self.assertEqual(len(self._audit("reward_video_failed")), 1)


if __name__ == "__main__":
    unittest.main()
