"""Business rule: video is Rewarded-only; Banner advertisements are image-only.

Covers the valid/invalid type+media combinations, server-side rejection of
Banner + Video (manually crafted requests), the Rewarded Video → Banner edit
cleanup (uploaded video removed via the existing asset helper), the form UI
hooks that hide/disable video for Banner, and backward compatibility.
"""

import io
import os
import shutil
import tempfile
import unittest
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 import services as ad_admin
from app.extensions import db
from app.models import (
    Advertisement,
    AdvertisementMediaType,
    AdvertisementPlacement,
    AdvertisementType,
    User,
    UserRole,
)


class _FakeField:
    def __init__(self, data):
        self.data = data


class _FakeForm:
    """Minimal stand-in for AdvertisementForm for unit-testing video_upload_error."""

    def __init__(self, *, media_type, advertisement_type, video=None, remove_video=False):
        self.media_type = _FakeField(media_type)
        self.advertisement_type = _FakeField(advertisement_type)
        self.video = _FakeField(video)
        self.remove_video = _FakeField(remove_video)


class BannerVideoRuleTests(unittest.TestCase):
    def setUp(self):
        self.app = create_app("testing")
        self.ctx = self.app.app_context()
        self.ctx.push()
        db.create_all()
        self._upload_dir = Path(tempfile.mkdtemp())
        self.app.config["ADVERTISEMENT_UPLOAD_FOLDER"] = self._upload_dir
        u = User(email="a@c.edu", password_hash=generate_password_hash("Passw0rd!"),
                 is_verified=True, role=UserRole.SUPER_ADMIN)
        db.session.add(u)
        db.session.commit()
        self.client = self.app.test_client()
        self.client.post("/login", data={"email": "a@c.edu", "password": "Passw0rd!"})

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

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

    def _form(self, **overrides):
        data = {"title": "Ad", "advertisement_type": "banner", "placement": "landing",
                "media_type": "image", "priority": "5", "is_active": "y", "submit": "Save"}
        data.update(overrides)
        return data

    def _post_new(self, **overrides):
        return self.client.post("/admin/advertisements/new", data=self._form(**overrides),
                                content_type="multipart/form-data", follow_redirects=True)

    def _get(self, title):
        return db.session.scalar(db.select(Advertisement).filter_by(title=title))

    def _stored(self, media_url):
        return self._upload_dir / os.path.basename(media_url)

    # --- valid combinations ----------------------------------------------------
    def test_banner_image_succeeds(self):
        self._post_new(title="BI", advertisement_type="banner", placement="landing",
                       media_type="image", icon="bi-stars")
        ad = self._get("BI")
        self.assertIsNotNone(ad)
        self.assertEqual(ad.advertisement_type, AdvertisementType.BANNER)
        self.assertEqual(ad.media_type, AdvertisementMediaType.IMAGE)

    def test_rewarded_image_succeeds(self):
        self._post_new(title="RI", advertisement_type="rewarded", placement="landing",
                       media_type="image", icon="bi-stars")
        ad = self._get("RI")
        self.assertIsNotNone(ad)
        self.assertEqual(ad.media_type, AdvertisementMediaType.IMAGE)

    def test_rewarded_video_succeeds(self):
        self._post_new(title="RV", advertisement_type="rewarded", placement="landing",
                       media_type="video", video_duration="15", video=self._mp4())
        ad = self._get("RV")
        self.assertIsNotNone(ad)
        self.assertTrue(ad.is_video)
        self.assertTrue(ad.media_url.endswith(".mp4"))

    # --- invalid combination ---------------------------------------------------
    def test_banner_video_rejected(self):
        resp = self._post_new(title="BV", advertisement_type="banner", placement="landing",
                              media_type="video", video=self._mp4())
        self.assertIn("only supported for rewarded advertisements", resp.get_data(as_text=True).lower())
        self.assertIsNone(self._get("BV"))   # not saved

    def test_video_upload_error_unit(self):
        # Banner + Video → rejected regardless of file presence.
        self.assertEqual(
            ad_admin.video_upload_error(_FakeForm(media_type="video", advertisement_type="banner"), None),
            "Video advertisements are only supported for Rewarded advertisements.",
        )
        # Banner + Image → fine.
        self.assertIsNone(
            ad_admin.video_upload_error(_FakeForm(media_type="image", advertisement_type="banner"), None))
        # Rewarded + Video with no file/existing → the required-file message, not the type message.
        self.assertEqual(
            ad_admin.video_upload_error(_FakeForm(media_type="video", advertisement_type="rewarded"), None),
            "A video file is required for video advertisements.",
        )

    # --- Rewarded Video → Banner edit cleanup ---------------------------------
    def test_rewarded_video_to_banner_removes_video(self):
        self._post_new(title="Flip", advertisement_type="rewarded", placement="landing",
                       media_type="video", video_duration="15", video=self._mp4())
        ad = self._get("Flip")
        stored = self._stored(ad.media_url)
        self.assertTrue(stored.is_file())
        # Edit to Banner + Image (exactly what the form JS submits for Banner).
        self.client.post(f"/admin/advertisements/{ad.public_id}/edit",
                         data=self._form(title="Flip", advertisement_type="banner", placement="landing",
                                         media_type="image", icon="bi-stars"),
                         content_type="multipart/form-data", follow_redirects=True)
        db.session.refresh(ad)
        self.assertEqual(ad.advertisement_type, AdvertisementType.BANNER)
        self.assertEqual(ad.media_type, AdvertisementMediaType.IMAGE)
        self.assertIsNone(ad.media_url)
        self.assertIsNone(ad.video_duration)
        self.assertFalse(stored.is_file())   # orphan video cleaned up

    def test_banner_crafted_video_normalised_to_image_defensively(self):
        # Even if validation were bypassed, _apply_form must never persist a video
        # banner: media_type is normalised to image for non-rewarded types.
        ad = Advertisement()
        form = _FakeForm(media_type="video", advertisement_type="banner")
        # Add the scalar fields _apply_form needs.
        for name, val in [("title", "X"), ("description", ""), ("redirect_url", ""),
                          ("placement", "landing"), ("priority", 5), ("icon", ""),
                          ("start_at", None), ("end_at", None), ("is_active", True),
                          ("video_duration", 30)]:
            setattr(form, name, _FakeField(val))
        ad_admin._apply_form(ad, form)
        self.assertEqual(ad.media_type, AdvertisementMediaType.IMAGE)
        self.assertIsNone(ad.video_duration)

    # --- form UI hooks ---------------------------------------------------------
    def test_form_has_type_media_hooks(self):
        html = self.client.get("/admin/advertisements/new").get_data(as_text=True)
        self.assertIn("data-ad-banner-note", html)                     # banner-only note element
        self.assertIn("Banner advertisements support images only", html)
        self.assertIn('data-ad-type="1"', html)                        # type select drives the JS
        self.assertIn("data-ad-video-fields", html)                    # video controls exist (JS-toggled)
        self.assertIn("displayed throughout the marketplace", html)    # banner help
        self.assertIn("unlock additional chat messages", html)         # rewarded help

    def test_reward_format_renamed_and_hidden_by_default(self):
        html = self.client.get("/admin/advertisements/new").get_data(as_text=True)
        self.assertIn("Reward Format", html)                           # renamed from "Media type"
        self.assertIn('d-none" data-ad-reward-format', html)           # hidden until Rewarded (JS)
        self.assertIn("Image Advertisement", html)                     # clearer option labels
        self.assertIn("Video Advertisement", html)
        self.assertIn("Choose whether users will watch", html)         # rewarded help copy

    def test_edit_rewarded_video_preselects_video(self):
        self._post_new(title="RV", advertisement_type="rewarded", placement="landing",
                       media_type="video", video_duration="15", video=self._mp4())
        ad = self._get("RV")
        html = self.client.get(f"/admin/advertisements/{ad.public_id}/edit").get_data(as_text=True)
        self.assertIn('<option selected value="video">', html)         # Reward Format = Video

    def test_edit_rewarded_image_preselects_image(self):
        self._post_new(title="RI", advertisement_type="rewarded", placement="landing",
                       media_type="image", icon="bi-stars")
        ad = self._get("RI")
        html = self.client.get(f"/admin/advertisements/{ad.public_id}/edit").get_data(as_text=True)
        self.assertIn('<option selected value="image">', html)         # Reward Format = Image

    def test_edit_banner_keeps_reward_format_hidden(self):
        self._post_new(title="Bnr", advertisement_type="banner", placement="landing",
                       media_type="image", icon="bi-stars")
        ad = self._get("Bnr")
        html = self.client.get(f"/admin/advertisements/{ad.public_id}/edit").get_data(as_text=True)
        self.assertIn('d-none" data-ad-reward-format', html)           # selector stays hidden for banner
        self.assertIn('<option selected value="image">', html)         # media_type still submits image

    # --- backward compatibility -----------------------------------------------
    def test_existing_rewarded_video_still_edits(self):
        self._post_new(title="Keep", advertisement_type="rewarded", placement="landing",
                       media_type="video", video_duration="15", video=self._mp4())
        ad = self._get("Keep")
        media_url = ad.media_url
        # Re-save without a new file (keep existing video) — must remain a video.
        self.client.post(f"/admin/advertisements/{ad.public_id}/edit",
                         data=self._form(title="Keep Renamed", advertisement_type="rewarded",
                                         placement="landing", media_type="video", video_duration="15"),
                         content_type="multipart/form-data", follow_redirects=True)
        db.session.refresh(ad)
        self.assertEqual(ad.title, "Keep Renamed")
        self.assertTrue(ad.is_video)
        self.assertEqual(ad.media_url, media_url)

    def test_existing_banner_image_unaffected(self):
        self._post_new(title="Banner", advertisement_type="banner", placement="marketplace",
                       media_type="image", icon="bi-stars")
        ad = self._get("Banner")
        self.client.post(f"/admin/advertisements/{ad.public_id}/edit",
                         data=self._form(title="Banner", advertisement_type="banner", placement="sidebar",
                                         media_type="image", icon="bi-laptop"),
                         content_type="multipart/form-data", follow_redirects=True)
        db.session.refresh(ad)
        self.assertEqual(ad.placement, AdvertisementPlacement.SIDEBAR)
        self.assertEqual(ad.media_type, AdvertisementMediaType.IMAGE)
        self.assertIsNone(ad.media_url)


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