"""
Regression tests for the platform-aware trial duration feature
(15 days for tizen/webos, 30 days for everything else) and admin
endpoints around /api/admin/device-trials.

Covers the 9 scenarios in the iteration_17 review request:
  1. Tizen → trial_days == 15
  2. WebOS → trial_days == 15
  3. Capacitor (Android phone) → trial_days == 30
  4. Missing platform field (legacy client) → trial_days == 30
  5. Reinstall persistence (same hardware_id twice keeps first_seen_at)
  6. Backdated 20-day Tizen device → trial_expired=true, days_remaining=0
  7. Admin PATCH is_premium=true persists; trial/status reports premium
  8. Admin DELETE /device-trials/{hwid} removes the device row
  9. Admin endpoints reject unauthenticated callers (401/403)
"""
import os
import time
import uuid
import subprocess
from datetime import datetime, timezone, timedelta

import pytest
import requests

BASE_URL = (
    os.environ.get("BACKEND_URL")
    or os.environ.get("REACT_APP_BACKEND_URL")
    or "http://localhost:8001"
).rstrip("/")

ADMIN_USER = os.environ.get("ADMIN_USERNAME", "admin")
ADMIN_PASS = os.environ.get("ADMIN_PASSWORD", "admin123")
DB_NAME = os.environ.get("DB_NAME", "nadiplayer_test")


# ---------- Fixtures ----------
@pytest.fixture(scope="session")
def api():
    s = requests.Session()
    s.headers.update({"Content-Type": "application/json"})
    return s


@pytest.fixture(scope="session")
def admin_token(api):
    r = api.post(
        f"{BASE_URL}/api/auth/login",
        json={"username": ADMIN_USER, "password": ADMIN_PASS},
    )
    assert r.status_code == 200, f"admin login failed: {r.status_code} {r.text}"
    return r.json()["access_token"]


@pytest.fixture(scope="session")
def admin_client(api, admin_token):
    s = requests.Session()
    s.headers.update(
        {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {admin_token}",
        }
    )
    return s


@pytest.fixture
def fresh_hw():
    """Unique TEST_ prefixed hardware_id. Cleanup is best-effort via mongosh."""
    hw = f"TEST_HW_{uuid.uuid4().hex[:10]}"
    yield hw
    try:
        subprocess.run(
            [
                "mongosh", DB_NAME, "--quiet", "--eval",
                f'db.device_trials.deleteOne({{hardware_id: "{hw}"}})',
            ],
            capture_output=True, text=True, timeout=5,
        )
    except Exception:
        pass


# ---------- Platform → trial_days matrix ----------
class TestPlatformTrialDays:
    """Verify _trial_days(platform) is wired into the response."""

    def test_tizen_returns_15(self, api, fresh_hw):
        r = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["trial_days"] == 15, f"expected 15 for tizen, got {d['trial_days']}"
        assert d["days_remaining"] == 15
        assert d["trial_expired"] is False
        assert d["hardware_id"] == fresh_hw

    def test_webos_returns_15(self, api, fresh_hw):
        r = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "webos"},
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["trial_days"] == 15
        assert d["days_remaining"] == 15

    def test_capacitor_returns_30(self, api, fresh_hw):
        r = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "capacitor"},
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["trial_days"] == 30
        assert d["days_remaining"] == 30

    def test_legacy_no_platform_returns_30(self, api, fresh_hw):
        """Legacy clients that don't send `platform` should still get the
        historical 30-day trial."""
        r = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw},
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["trial_days"] == 30
        assert d["days_remaining"] == 30

    def test_platform_case_insensitive(self, api, fresh_hw):
        """Spec: platform string is .strip().lower()'d server-side."""
        r = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "Tizen"},
        )
        assert r.status_code == 200, r.text
        assert r.json()["trial_days"] == 15

    def test_unknown_platform_returns_30(self, api, fresh_hw):
        """A platform string not in the TV allow-list defaults to 30."""
        r = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "roku"},
        )
        assert r.status_code == 200, r.text
        assert r.json()["trial_days"] == 30

    def test_platform_persists_across_calls(self, api, fresh_hw):
        """Second call WITHOUT platform must still resolve to 15 because
        the row already stores platform='tizen'."""
        r1 = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        ).json()
        assert r1["trial_days"] == 15
        # 2nd call: legacy payload, no platform — server must remember.
        r2 = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw},
        ).json()
        assert r2["trial_days"] == 15, (
            f"platform did not persist; got {r2['trial_days']} on 2nd call"
        )
        assert r2["first_seen_at"] == r1["first_seen_at"]


# ---------- Anti-reinstall: first_seen_at stickiness ----------
class TestReinstallPersistence:
    def test_same_hw_keeps_first_seen_at(self, api, fresh_hw):
        r1 = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        ).json()
        time.sleep(1.1)
        # Simulate a reinstall: same hwid, fresh app payload
        r2 = api.post(
            f"{BASE_URL}/api/trial/status",
            json={
                "hardware_id": fresh_hw,
                "platform": "tizen",
                "app_version": "9.9.9",
            },
        ).json()
        assert r1["first_seen_at"] == r2["first_seen_at"], (
            "first_seen_at was reset on second call — anti-reinstall is broken"
        )
        assert r2["last_seen_at"] > r1["last_seen_at"]
        # Trial clock not reset
        assert r2["trial_days"] == 15


# ---------- Backdated Tizen → expired ----------
class TestBackdatedTrialExpired:
    def test_tizen_after_20_days_is_expired(self, api, fresh_hw):
        # Seed the device
        r = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        )
        assert r.status_code == 200

        # Backdate first_seen_at to 20 days ago via mongosh
        backdate_iso = (
            datetime.now(timezone.utc) - timedelta(days=20)
        ).isoformat()
        proc = subprocess.run(
            [
                "mongosh", DB_NAME, "--quiet", "--eval",
                (
                    f'db.device_trials.updateOne('
                    f'{{hardware_id: "{fresh_hw}"}}, '
                    f'{{$set: {{first_seen_at: "{backdate_iso}"}}}})'
                ),
            ],
            capture_output=True, text=True, timeout=10,
        )
        if proc.returncode != 0:
            pytest.skip(f"mongosh unavailable: {proc.stderr}")

        # Re-query
        r2 = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        )
        assert r2.status_code == 200, r2.text
        d = r2.json()
        assert d["trial_days"] == 15
        assert d["trial_expired"] is True, (
            f"expected trial_expired=True at 20d for tizen; got {d}"
        )
        assert d["days_remaining"] == 0
        assert d["days_used"] >= 15


# ---------- Admin: list / patch / delete ----------
class TestAdminDeviceTrials:
    def test_admin_list_devices_ok(self, admin_client):
        r = admin_client.get(f"{BASE_URL}/api/admin/device-trials?limit=10")
        assert r.status_code == 200, r.text
        body = r.json()
        assert isinstance(body, list)
        if body:
            # Each row has computed fields
            row = body[0]
            for key in (
                "hardware_id", "first_seen_at", "last_seen_at",
                "is_premium", "days_used", "days_remaining", "trial_expired",
            ):
                assert key in row, f"missing key {key} in admin row"

    def test_admin_patch_is_premium_unlocks_device(self, api, admin_client, fresh_hw):
        # Seed a tizen device that the admin will mark premium
        api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        )
        # Admin flips is_premium=true
        pr = admin_client.patch(
            f"{BASE_URL}/api/admin/device-trials/{fresh_hw}",
            json={"is_premium": True},
        )
        assert pr.status_code == 200, pr.text
        assert pr.json()["is_premium"] is True

        # Now trial/status must echo is_premium=true so the frontend unlocks
        r = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["is_premium"] is True, "is_premium did not propagate to trial/status"

    def test_admin_patch_expired_premium_auto_clears(
        self, api, admin_client, fresh_hw
    ):
        """If premium_expiry is in the past, trial/status must auto-clear
        is_premium back to False."""
        api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        )
        past = (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
        pr = admin_client.patch(
            f"{BASE_URL}/api/admin/device-trials/{fresh_hw}",
            json={"is_premium": True, "premium_expiry": past},
        )
        assert pr.status_code == 200, pr.text
        # trial/status: auto-expire kicks in
        r = api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        ).json()
        assert r["is_premium"] is False, (
            f"expired premium did not auto-clear: {r}"
        )

    def test_admin_patch_invalid_expiry_returns_400(self, admin_client, api, fresh_hw):
        api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        )
        pr = admin_client.patch(
            f"{BASE_URL}/api/admin/device-trials/{fresh_hw}",
            json={"premium_expiry": "not-a-date"},
        )
        assert pr.status_code == 400, pr.text

    def test_admin_patch_unknown_device_returns_404(self, admin_client):
        pr = admin_client.patch(
            f"{BASE_URL}/api/admin/device-trials/TEST_HW_DOES_NOT_EXIST_{uuid.uuid4().hex[:6]}",
            json={"is_premium": True},
        )
        assert pr.status_code == 404

    def test_admin_delete_removes_device(self, api, admin_client, fresh_hw):
        api.post(
            f"{BASE_URL}/api/trial/status",
            json={"hardware_id": fresh_hw, "platform": "tizen"},
        )
        dr = admin_client.delete(
            f"{BASE_URL}/api/admin/device-trials/{fresh_hw}"
        )
        assert dr.status_code == 200, dr.text
        assert dr.json().get("ok") is True

        # Second delete → 404
        dr2 = admin_client.delete(
            f"{BASE_URL}/api/admin/device-trials/{fresh_hw}"
        )
        assert dr2.status_code == 404

    def test_admin_delete_unknown_device_returns_404(self, admin_client):
        dr = admin_client.delete(
            f"{BASE_URL}/api/admin/device-trials/TEST_HW_NO_SUCH_{uuid.uuid4().hex[:6]}"
        )
        assert dr.status_code == 404


# ---------- Admin auth gate ----------
class TestAdminAuthRequired:
    def test_list_requires_admin(self, api):
        r = api.get(f"{BASE_URL}/api/admin/device-trials")
        assert r.status_code in (401, 403), r.text

    def test_patch_requires_admin(self, api):
        r = api.patch(
            f"{BASE_URL}/api/admin/device-trials/whatever",
            json={"is_premium": True},
        )
        assert r.status_code in (401, 403), r.text

    def test_delete_requires_admin(self, api):
        r = api.delete(f"{BASE_URL}/api/admin/device-trials/whatever")
        assert r.status_code in (401, 403), r.text

    def test_non_admin_user_rejected(self, api, admin_client):
        """A regular user's bearer token must NOT access admin endpoints."""
        suffix = uuid.uuid4().hex[:6]
        username = f"trial_user_{suffix}"
        password = "pw12345"
        cr = admin_client.post(
            f"{BASE_URL}/api/admin/users",
            json={
                "username": username, "password": password,
                "days": 30, "xtream_mode": "shared",
            },
        )
        if cr.status_code not in (200, 201):
            pytest.skip(f"could not create test user: {cr.status_code} {cr.text}")
        user_id = cr.json()["id"]
        try:
            lr = api.post(
                f"{BASE_URL}/api/auth/login",
                json={"username": username, "password": password},
            )
            assert lr.status_code == 200, lr.text
            user_token = lr.json()["access_token"]
            r = requests.get(
                f"{BASE_URL}/api/admin/device-trials",
                headers={"Authorization": f"Bearer {user_token}"},
            )
            assert r.status_code == 403, r.text
        finally:
            admin_client.delete(f"{BASE_URL}/api/admin/users/{user_id}")
