"""SaaS/Admin iteration tests: packages CRUD, admin endpoints, CMS settings,
quota gating (402), checkout, list-folders, folder selection e2e, DB-backed lockout."""
import time
import uuid

import pytest
import requests

from conftest import API, IMAP, mig_payload

LOCAL_API = "http://localhost:8001/api"


# ---------- helpers ----------
def new_user_session():
    email = f"TEST_saas_{uuid.uuid4().hex[:8]}@test.com"
    r = requests.post(f"{API}/auth/register", json={"email": email, "password": "Passw0rd!23", "name": "TEST User"}, timeout=30)
    assert r.status_code == 200, r.text
    tok = r.json()["token"]
    s = requests.Session()
    s.headers.update({"Content-Type": "application/json", "Authorization": f"Bearer {tok}"})
    return email, s


# ---------- Public packages + settings ----------
class TestPublicCatalog:
    def test_packages_seeded(self):
        r = requests.get(f"{API}/packages", timeout=30)
        assert r.status_code == 200
        pkgs = r.json()
        names = {p["name"]: p for p in pkgs}
        assert {"Starter", "Professional", "Business"} <= set(names), names.keys()
        assert names["Starter"]["price"] == 99900
        assert names["Professional"]["price"] == 299900
        assert names["Business"]["price"] == 599900
        for p in pkgs:
            for k in ("id", "max_migrations", "max_mailboxes", "validity_days", "storage_per_mailbox_gb", "is_active"):
                assert k in p
            assert p["is_active"] is True
            assert "_id" not in p

    def test_public_settings(self):
        r = requests.get(f"{API}/settings", timeout=30)
        assert r.status_code == 200
        d = r.json()
        for k in ("site_name", "brand_color", "hero_title", "pricing_title", "logo_url", "footer_text"):
            assert k in d
        assert d["site_name"]


# ---------- Admin auth guard ----------
class TestAdminGuard:
    def test_normal_user_forbidden(self):
        _, s = new_user_session()
        for path in ("/admin/stats", "/admin/users", "/admin/packages", "/admin/subscriptions"):
            r = s.get(f"{API}{path}", timeout=30)
            assert r.status_code == 403, f"{path} -> {r.status_code}"
        r = s.put(f"{API}/admin/settings", json={"site_name": "Hacked"}, timeout=30)
        assert r.status_code == 403

    def test_unauthenticated_rejected(self):
        r = requests.get(f"{API}/admin/stats", timeout=30)
        assert r.status_code in (401, 403)


# ---------- Admin dashboards ----------
class TestAdminEndpoints:
    def test_stats(self, client):
        r = client.get(f"{API}/admin/stats", timeout=30)
        assert r.status_code == 200
        d = r.json()
        for k in ("users", "migrations", "active_subscriptions", "revenue_paise"):
            assert k in d and isinstance(d[k], int), d
        assert d["users"] >= 1

    def test_users_list(self, client, test_credentials):
        r = client.get(f"{API}/admin/users", timeout=30)
        assert r.status_code == 200
        users = r.json()
        admin = [u for u in users if u["email"] == test_credentials["email"]]
        assert admin, "admin missing from /admin/users"
        assert admin[0]["role"] == "admin"
        for k in ("id", "email", "name", "role", "plan", "migrations"):
            assert k in admin[0]

    def test_new_user_appears(self, client):
        email, _ = new_user_session()
        r = client.get(f"{API}/admin/users", timeout=30)
        assert email.lower() in [u["email"] for u in r.json()]

    def test_subscriptions_list(self, client):
        r = client.get(f"{API}/admin/subscriptions", timeout=30)
        assert r.status_code == 200
        assert isinstance(r.json(), list)


# ---------- Packages CRUD ----------
class TestPackagesCRUD:
    def test_crud_and_persistence(self, client):
        payload = {"name": "TEST_QA Pack", "description": "qa", "price": 12300,
                   "max_migrations": 7, "storage_per_mailbox_gb": 11, "max_mailboxes": 3,
                   "validity_days": 15, "is_active": True, "sort_order": 99}
        r = client.post(f"{API}/admin/packages", json=payload, timeout=30)
        assert r.status_code == 200, r.text
        pkg = r.json()
        pid = pkg["id"]
        try:
            assert pkg["name"] == payload["name"] and pkg["price"] == 12300
            # visible in admin list and public list
            assert pid in [p["id"] for p in client.get(f"{API}/admin/packages", timeout=30).json()]
            assert pid in [p["id"] for p in requests.get(f"{API}/packages", timeout=30).json()]

            # UPDATE
            upd = dict(payload, name="TEST_QA Pack v2", price=45600, max_migrations=-1)
            r = client.put(f"{API}/admin/packages/{pid}", json=upd, timeout=30)
            assert r.status_code == 200, r.text
            assert r.json()["name"] == "TEST_QA Pack v2"
            got = [p for p in requests.get(f"{API}/packages", timeout=30).json() if p["id"] == pid][0]
            assert got["name"] == "TEST_QA Pack v2" and got["price"] == 45600 and got["max_migrations"] == -1
        finally:
            d = client.delete(f"{API}/admin/packages/{pid}", timeout=30)
            assert d.status_code in (200, 204), d.text
        assert pid not in [p["id"] for p in client.get(f"{API}/admin/packages", timeout=30).json()]

    def test_update_missing_package_404(self, client):
        r = client.put(f"{API}/admin/packages/nope-{uuid.uuid4().hex[:6]}",
                       json={"name": "x", "price": 100, "max_migrations": 1,
                             "storage_per_mailbox_gb": 1, "max_mailboxes": 1, "validity_days": 1}, timeout=30)
        assert r.status_code == 404, r.status_code

    @pytest.mark.xfail(reason="DELETE /admin/packages/{id} returns 200 for unknown id (no existence check)", strict=False)
    def test_delete_missing_package_404(self, client):
        r = client.delete(f"{API}/admin/packages/nope-{uuid.uuid4().hex[:6]}", timeout=30)
        assert r.status_code == 404, r.status_code

    def test_inactive_package_hidden_publicly(self, client):
        payload = {"name": "TEST_Hidden", "price": 100, "max_migrations": 1,
                   "storage_per_mailbox_gb": 1, "max_mailboxes": 1, "validity_days": 1,
                   "is_active": False, "sort_order": 50}
        pid = client.post(f"{API}/admin/packages", json=payload, timeout=30).json()["id"]
        try:
            pub = [p["id"] for p in requests.get(f"{API}/packages", timeout=30).json()]
            assert pid not in pub, "inactive package exposed on /api/packages"
            adm = [p["id"] for p in client.get(f"{API}/admin/packages", timeout=30).json()]
            assert pid in adm
        finally:
            client.delete(f"{API}/admin/packages/{pid}", timeout=30)


# ---------- CMS settings ----------
class TestCmsSettings:
    def test_update_and_revert(self, client):
        orig = requests.get(f"{API}/settings", timeout=30).json()
        try:
            new = dict(orig, site_name="TEST_MailShiftQA", brand_color="#DC2626", hero_title="QA Hero")
            r = client.put(f"{API}/admin/settings", json=new, timeout=30)
            assert r.status_code == 200, r.text
            got = requests.get(f"{API}/settings", timeout=30).json()
            assert got["site_name"] == "TEST_MailShiftQA"
            assert got["brand_color"] == "#DC2626"
            assert got["hero_title"] == "QA Hero"
        finally:
            client.put(f"{API}/admin/settings", json=orig, timeout=30)
        back = requests.get(f"{API}/settings", timeout=30).json()
        assert back["site_name"] == orig["site_name"]
        assert back["brand_color"] == orig["brand_color"]


# ---------- Subscription gating ----------
class TestQuotaGating:
    def test_new_user_no_plan_and_402(self):
        _, s = new_user_session()
        r = s.get(f"{API}/me/subscription", timeout=30)
        assert r.status_code == 200
        d = r.json()
        assert d.get("is_admin") in (False, None)
        assert not d.get("subscription") or not d["subscription"].get("is_active")

        r = s.post(f"{API}/migrations", json=mig_payload("TEST_nogate"), timeout=60)
        assert r.status_code == 402, f"expected 402, got {r.status_code}: {r.text[:200]}"
        assert "plan" in str(r.json().get("detail", "")).lower()

    def test_bulk_also_gated(self):
        _, s = new_user_session()
        r = s.post(f"{API}/migrations/bulk", json={"migrations": [mig_payload("TEST_bulkgate")], "auto_start": False}, timeout=60)
        assert r.status_code == 402, r.status_code

    def test_admin_bypasses_gating(self, client, created_ids):
        r = client.get(f"{API}/me/subscription", timeout=30)
        assert r.status_code == 200 and r.json().get("is_admin") is True
        r = client.post(f"{API}/migrations", json=mig_payload("TEST_admin_bypass"), timeout=60)
        assert r.status_code == 200, r.text
        created_ids.append(r.json()["id"])


# ---------- Checkout ----------
class TestCheckout:
    def test_payment_config(self, client):
        r = client.get(f"{API}/payment-config", timeout=30)
        assert r.status_code == 200
        d = r.json()
        assert "key_id" in d or "enabled" in d, d

    def test_order_invalid_package(self, client):
        r = client.post(f"{API}/checkout/order", json={"package_id": "does-not-exist"}, timeout=40)
        assert r.status_code in (400, 404), r.status_code

    def test_verify_invalid_signature(self, client):
        r = client.post(f"{API}/checkout/verify", json={
            "razorpay_order_id": "order_TESTinvalid",
            "razorpay_payment_id": "pay_TESTinvalid",
            "razorpay_signature": "deadbeef",
        }, timeout=30)
        assert r.status_code in (400, 404), f"{r.status_code}: {r.text[:200]}"


# ---------- Folder listing + selection ----------
class TestFolders:
    def test_list_folders(self, client):
        r = client.post(f"{API}/list-folders", json={
            "host": IMAP["src"]["host"], "port": IMAP["src"]["port"],
            "email": IMAP["src"]["email"], "password": IMAP["src"]["password"], "ssl": False,
        }, timeout=60)
        assert r.status_code == 200, r.text
        d = r.json()
        folders = d.get("folders", d)
        names = [f if isinstance(f, str) else f.get("name") for f in folders]
        assert "INBOX" in names, names
        assert any("Work" in (n or "") for n in names), names

    def test_list_folders_bad_creds(self, client):
        r = client.post(f"{API}/list-folders", json={
            "host": IMAP["src"]["host"], "port": IMAP["src"]["port"],
            "email": "user1", "password": "wrongpass", "ssl": False,
        }, timeout=60)
        assert r.status_code == 200, r.status_code
        d = r.json()
        assert d["ok"] is False, d
        assert d["folders"] == []

    def test_selected_folders_e2e_only_inbox(self, client, created_ids):
        payload = dict(mig_payload("TEST_folder_sel"), selected_folders=["INBOX"])
        r = client.post(f"{API}/migrations", json=payload, timeout=60)
        assert r.status_code == 200, r.text
        mid = r.json()["id"]
        created_ids.append(mid)
        assert client.post(f"{API}/migrations/{mid}/start", timeout=30).status_code == 200

        deadline = time.time() + 90
        mig = {}
        while time.time() < deadline:
            mig = client.get(f"{API}/migrations/{mid}", timeout=30).json()
            if mig["status"] in ("completed", "failed"):
                break
            time.sleep(2)
        assert mig.get("status") == "completed", mig
        assert mig.get("total_emails") == 5, mig
        assert mig.get("migrated_emails") == 5, mig.get("migrated_emails")

        folders = client.get(f"{API}/migrations/{mid}/folders", timeout=30).json()
        fnames = [f.get("name") or f.get("folder") for f in folders]
        assert len(folders) == 1, fnames
        assert "INBOX" in str(fnames[0])
        assert not any("Work" in str(n) for n in fnames), fnames


# ---------- Recurring ----------
class TestRecurring:
    def test_recurring_fields_persisted(self, client, created_ids):
        payload = dict(mig_payload("TEST_recurring"), recurring=True, recurring_minutes=45)
        r = client.post(f"{API}/migrations", json=payload, timeout=60)
        assert r.status_code == 200, r.text
        mid = r.json()["id"]
        created_ids.append(mid)
        d = client.get(f"{API}/migrations/{mid}", timeout=30).json()
        assert d.get("recurring") is True
        assert d.get("recurring_minutes") == 45


# ---------- DB-backed brute force lockout ----------
class TestBruteForceLockout:
    def test_lockout_after_five(self):
        email = f"TEST_bf_{uuid.uuid4().hex[:8]}@test.com"
        codes = []
        for _ in range(5):
            r = requests.post(f"{LOCAL_API}/auth/login", json={"email": email, "password": "wrong"}, timeout=30)
            codes.append(r.status_code)
        assert codes == [401] * 5, codes
        r = requests.post(f"{LOCAL_API}/auth/login", json={"email": email, "password": "wrong"}, timeout=30)
        assert r.status_code == 429, f"{r.status_code}: {r.text[:200]}"

    def test_lockout_persisted_in_db(self):
        """Locked identifier must remain locked (DB-backed, survives new requests/processes)."""
        email = f"TEST_bf2_{uuid.uuid4().hex[:8]}@test.com"
        pw = "Passw0rd!23"
        assert requests.post(f"{API}/auth/register", json={"email": email, "password": pw, "name": "TEST"}, timeout=30).status_code == 200
        for _ in range(5):
            requests.post(f"{LOCAL_API}/auth/login", json={"email": email, "password": "wrong"}, timeout=30)
        # even the correct password must be blocked while locked
        r = requests.post(f"{LOCAL_API}/auth/login", json={"email": email, "password": pw}, timeout=30)
        assert r.status_code == 429, f"{r.status_code}: {r.text[:200]}"
        # still locked on a subsequent request
        r2 = requests.post(f"{LOCAL_API}/auth/login", json={"email": email, "password": pw}, timeout=30)
        assert r2.status_code == 429

    def test_other_identifier_unaffected(self, test_credentials):
        r = requests.post(f"{LOCAL_API}/auth/login", json=test_credentials, timeout=30)
        assert r.status_code == 200, r.text
