"""Rewarded video advertisement tests.

Covers the video extension to the rewarded-ad system: model media fields, the
admin create/edit workflow, modal rendering (video vs. image), backward
compatibility with image ads, and the (unchanged) reward-granting flow.
"""

import io
import os
import shutil
import tempfile
import unittest
from decimal import Decimal
from pathlib import Path

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 duplicate_advertisement
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 (
    Advertisement,
    AdvertisementMediaType,
    AdvertisementPlacement,
    AdvertisementType,
    Category,
    Conversation,
    ConversationStatus,
    Listing,
    ListingCondition,
    ListingStatus,
    ListingVisibility,
    ReviewStatus,
    User,
    UserRole,
)


class RewardedVideoAdTests(unittest.TestCase):
    def setUp(self):
        self.app = create_app("testing")
        self.ctx = self.app.app_context()
        self.ctx.push()
        db.create_all()
        reset_replay_guard()
        reset_rate_limits()
        # Route uploaded files to a throwaway dir (never the real static folder).
        self._upload_dir = Path(tempfile.mkdtemp())
        self.app.config["ADVERTISEMENT_UPLOAD_FOLDER"] = self._upload_dir

    def _mp4_file(self, name="promo.mp4"):
        return (io.BytesIO(b"\x00\x00\x00\x20ftypisom" + b"\x00" * 16), name)

    def tearDown(self):
        reset_rate_limits()
        shutil.rmtree(self._upload_dir, ignore_errors=True)
        db.session.remove()
        db.drop_all()
        self.ctx.pop()

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

    def _category(self):
        cat = db.session.scalar(db.select(Category).filter_by(name="Books"))
        if cat is None:
            cat = Category(name="Books", description="d", is_active=True, display_order=1)
            db.session.add(cat)
            db.session.commit()
        return cat

    def _listing(self, seller):
        listing = Listing(
            sku=f"SKU-{seller.id}", seller_id=seller.id, category_id=self._category().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()
        return listing

    def _convo(self, buyer, seller):
        convo = Conversation(
            listing_id=self._listing(seller).id, buyer_id=buyer.id, seller_id=seller.id,
            status=ConversationStatus.ACTIVE,
        )
        db.session.add(convo)
        db.session.commit()
        return convo

    def _video_ad(self, *, url="https://cdn.example.com/ad.mp4", duration=15, image_path=None, active=True):
        ad = Advertisement(
            title="Watch & Learn", advertisement_type=AdvertisementType.REWARDED,
            placement=AdvertisementPlacement.CHAT, media_type=AdvertisementMediaType.VIDEO,
            media_url=url, video_duration=duration, image_path=image_path,
            is_active=active, priority=10,
        )
        db.session.add(ad)
        db.session.commit()
        return ad

    def _image_ad(self, *, active=True):
        ad = Advertisement(
            title="Sponsor Banner", advertisement_type=AdvertisementType.REWARDED,
            placement=AdvertisementPlacement.CHAT, image_path="/static/uploads/advertisements/x.png",
            is_active=active, priority=10,
        )
        db.session.add(ad)
        db.session.commit()
        return ad

    def _admin_client(self):
        self._user("admin@c.edu", UserRole.SUPER_ADMIN)
        c = self.app.test_client()
        c.post("/login", data={"email": "admin@c.edu", "password": "Passw0rd!"})
        return c

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

    # --- model -----------------------------------------------------------------
    def test_image_ad_defaults(self):
        ad = self._image_ad()
        self.assertEqual(ad.media_type, AdvertisementMediaType.IMAGE)
        self.assertTrue(ad.is_image)
        self.assertFalse(ad.is_video)
        self.assertEqual(ad.media_src, ad.image_path)

    def test_video_ad_properties(self):
        ad = self._video_ad(duration=25)
        self.assertTrue(ad.is_video)
        self.assertFalse(ad.is_image)
        self.assertEqual(ad.media_src, "https://cdn.example.com/ad.mp4")
        self.assertEqual(ad.resolved_video_duration, 25)

    def test_video_type_without_url_is_not_video(self):
        ad = Advertisement(title="x", placement=AdvertisementPlacement.CHAT,
                           media_type=AdvertisementMediaType.VIDEO, media_url=None)
        db.session.add(ad)
        db.session.commit()
        self.assertFalse(ad.is_video)          # defensive: no URL → not playable
        self.assertEqual(ad.resolved_video_duration, 20)  # default fallback

    # --- admin CRUD ------------------------------------------------------------
    def _base_form(self, **overrides):
        data = {
            "title": "New Ad", "advertisement_type": "rewarded", "placement": "landing",
            "media_type": "image", "priority": "5", "is_active": "y",
            "submit": "Save advertisement",
        }
        data.update(overrides)
        return data

    def test_admin_create_video_ad(self):
        client = self._admin_client()
        client.post("/admin/advertisements/new", data=self._base_form(
            title="Promo Video", media_type="video", video_duration="18",
            video=self._mp4_file(),
        ), content_type="multipart/form-data", follow_redirects=True)
        ad = db.session.scalar(db.select(Advertisement).filter_by(title="Promo Video"))
        self.assertIsNotNone(ad)
        self.assertEqual(ad.media_type, AdvertisementMediaType.VIDEO)
        self.assertIn("/uploads/advertisements/", ad.media_url)   # stored on our server
        self.assertTrue(ad.media_url.endswith(".mp4"))
        self.assertEqual(ad.video_duration, 18)

    def test_admin_create_image_ad_unchanged(self):
        client = self._admin_client()
        client.post("/admin/advertisements/new", data=self._base_form(
            title="Plain Banner", media_type="image", icon="bi-stars",
        ), follow_redirects=True)
        ad = db.session.scalar(db.select(Advertisement).filter_by(title="Plain Banner"))
        self.assertIsNotNone(ad)
        self.assertEqual(ad.media_type, AdvertisementMediaType.IMAGE)
        self.assertIsNone(ad.media_url)
        self.assertIsNone(ad.video_duration)

    def test_admin_video_requires_file(self):
        client = self._admin_client()
        resp = client.post("/admin/advertisements/new", data=self._base_form(
            title="No File Video", media_type="video",
        ), content_type="multipart/form-data", follow_redirects=True)
        self.assertIn("video file is required", resp.get_data(as_text=True).lower())
        self.assertIsNone(db.session.scalar(db.select(Advertisement).filter_by(title="No File Video")))

    def test_admin_edit_image_to_video(self):
        ad = self._image_ad()
        client = self._admin_client()
        client.post(f"/admin/advertisements/{ad.public_id}/edit", data=self._base_form(
            title="Sponsor Banner", media_type="video", video_duration="30",
            video=self._mp4_file(),
        ), content_type="multipart/form-data", follow_redirects=True)
        db.session.refresh(ad)
        self.assertEqual(ad.media_type, AdvertisementMediaType.VIDEO)
        self.assertIn("/uploads/advertisements/", ad.media_url)
        self.assertEqual(ad.video_duration, 30)

    def test_duplicate_copies_media_fields(self):
        ad = self._video_ad(duration=22)
        copy = duplicate_advertisement(ad)
        self.assertEqual(copy.media_type, AdvertisementMediaType.VIDEO)
        self.assertEqual(copy.media_url, ad.media_url)
        self.assertEqual(copy.video_duration, 22)
        self.assertFalse(copy.is_active)

    # --- modal rendering -------------------------------------------------------
    def _render_thread(self, ad_factory):
        buyer = self._user("buyer@c.edu")
        seller = self._user("seller@c.edu")
        convo = self._convo(buyer, seller)
        ad_factory()
        # Sanity: a free participant can watch an ad, so the modal is included.
        self.assertTrue(get_messaging_state(convo, buyer)["can_watch_ad"])
        html = self._login("buyer@c.edu").get(f"/app/chat/{convo.public_id}").get_data(as_text=True)
        return html

    def test_modal_renders_video(self):
        html = self._render_thread(lambda: self._video_ad(url="https://cdn.example.com/spot.mp4", duration=12))
        self.assertIn('data-ad-media-type="video"', html)
        self.assertIn("<video", html)
        self.assertIn("data-ad-video", html)
        self.assertIn("https://cdn.example.com/spot.mp4", html)
        self.assertIn("playsinline", html)
        self.assertIn("muted", html)
        self.assertIn('data-duration-seconds="12"', html)   # countdown = configured duration
        self.assertIn("data-ad-ring", html)                 # circular countdown present

    def test_modal_renders_image(self):
        html = self._render_thread(self._image_ad)
        self.assertIn('data-ad-media-type="image"', html)
        self.assertIn("rad-ad-img", html)
        self.assertNotIn("<video", html)
        self.assertIn("data-ad-ring", html)

    def test_modal_video_with_image_has_fallback(self):
        html = self._render_thread(lambda: self._video_ad(image_path="/static/uploads/advertisements/f.png"))
        self.assertIn("data-ad-fallback-img", html)   # JS can fall back to the image

    def test_modal_video_without_image_has_error_state(self):
        html = self._render_thread(lambda: self._video_ad(image_path=None))
        self.assertIn("data-ad-video-error", html)    # no fallback → error element exists
        self.assertNotIn("data-ad-fallback-img", html)

    # --- reward flow unchanged -------------------------------------------------
    def test_reward_granted_with_video_ad(self):
        reset_replay_guard()
        buyer = self._user("buyer@c.edu")
        seller = self._user("seller@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._video_ad()
        token = issue_reward_token(buyer.id, ad.public_id)
        before = get_messaging_state(convo, buyer)["total_remaining"]
        resp = self._login("buyer@c.edu").post(
            f"/app/chat/{convo.public_id}/watch-ad",
            data={"reward_token": token}, follow_redirects=True,
        )
        self.assertEqual(resp.status_code, 200)
        db.session.refresh(convo)
        after = get_messaging_state(convo, buyer)["total_remaining"]
        self.assertEqual(after - before, self.app.config["MESSAGES_PER_REWARDED_AD"])

    def test_reward_granted_with_image_ad_backward_compatible(self):
        reset_replay_guard()
        buyer = self._user("buyer@c.edu")
        seller = self._user("seller@c.edu")
        convo = self._convo(buyer, seller)
        ad = self._image_ad()
        token = issue_reward_token(buyer.id, ad.public_id)
        before = get_messaging_state(convo, buyer)["total_remaining"]
        self._login("buyer@c.edu").post(
            f"/app/chat/{convo.public_id}/watch-ad",
            data={"reward_token": token}, follow_redirects=True,
        )
        db.session.refresh(convo)
        after = get_messaging_state(convo, buyer)["total_remaining"]
        self.assertEqual(after - before, self.app.config["MESSAGES_PER_REWARDED_AD"])


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