"""Iteration 6 — Customer/Dealer Portal + Storefront + Invoice Verify
+ Version Manager + Licensing/Plans/Devices.

Covers all 22 cases listed in the review request. Uses admin session
fixture from conftest.py. Self-cleans test data (portal customer party,
sale_order invoices, devices, license activation reset back to trial).
"""
import time
import uuid
import pytest
import requests
from conftest import BASE_URL  # noqa: E402


# ---------- shared state across tests in a class -------------------------
@pytest.fixture(scope="module")
def state():
    return {}


# =========================================================================
# Version Manager
# =========================================================================
class TestVersion:
    """/api/version/* — public + admin"""

    def test_current(self):
        r = requests.get(f"{BASE_URL}/api/version/current", timeout=30)
        assert r.status_code == 200, r.text
        data = r.json()
        for k in ("version", "build_hash", "deployed_at", "server_time", "environment"):
            assert k in data, f"missing {k} in {data}"
        assert data["version"] == "3.7.0", f"expected 3.7.0, got {data['version']}"

    def test_check_outdated(self):
        r = requests.get(f"{BASE_URL}/api/version/check", params={"client_version": "3.0.0"}, timeout=30)
        assert r.status_code == 200
        d = r.json()
        assert d["update_available"] is True
        assert d["release"] is not None
        assert d["release"]["tag"] == "3.7.0"

    def test_check_uptodate(self):
        r = requests.get(f"{BASE_URL}/api/version/check", params={"client_version": "3.7.0"}, timeout=30)
        assert r.status_code == 200
        d = r.json()
        assert d["update_available"] is False

    def test_changelog(self):
        r = requests.get(f"{BASE_URL}/api/version/changelog", timeout=30)
        assert r.status_code == 200
        d = r.json()
        rels = d["releases"]
        assert len(rels) >= 10, f"expected >=10 releases, got {len(rels)}"
        # newest first
        assert rels[0]["tag"] == "3.7.0"
        for rel in rels:
            assert "tag" in rel and "date" in rel and "highlights" in rel

    def test_rollback_info_requires_admin(self):
        r = requests.get(f"{BASE_URL}/api/version/rollback-info", timeout=30)
        assert r.status_code in (401, 403), f"unauth call should be rejected, got {r.status_code}"

    def test_rollback_info_admin(self, admin_session):
        r = admin_session.get(f"{BASE_URL}/api/version/rollback-info", timeout=30)
        assert r.status_code == 200, r.text
        d = r.json()
        assert isinstance(d.get("instructions"), list) and len(d["instructions"]) >= 3
        assert isinstance(d.get("history"), list) and len(d["history"]) >= 10

    def test_check_now(self, admin_session):
        r = admin_session.post(
            f"{BASE_URL}/api/version/check-now",
            json={"client_version": "3.5.0"},
            timeout=30,
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["update_available"] is True
        assert d["current"] == "3.7.0"


# =========================================================================
# Licensing
# =========================================================================
class TestLicensing:
    """/api/license/*"""

    def test_plans_public(self, state):
        r = requests.get(f"{BASE_URL}/api/license/plans", timeout=30)
        assert r.status_code == 200, r.text
        d = r.json()
        plans = d["plans"]
        assert len(plans) == 4
        keys = {p["key"]: p for p in plans}
        assert set(keys.keys()) == {"trial", "basic", "pro", "enterprise"}
        assert keys["trial"]["price_inr"] == 0
        assert keys["basic"]["price_inr"] == 499
        assert keys["pro"]["price_inr"] == 999
        assert keys["enterprise"]["price_inr"] == 2499
        for p in plans:
            assert isinstance(p.get("features"), dict)
            assert "max_firms" in p

    def test_status_autoinit(self, admin_session, state):
        r = admin_session.get(f"{BASE_URL}/api/license/status", timeout=30)
        assert r.status_code == 200, r.text
        d = r.json()
        assert "plan" in d and "days_left" in d and "features" in d
        # store initial plan to restore later
        state["initial_plan"] = d["plan"]

    def test_generate_demo_key(self, admin_session, state):
        r = admin_session.post(
            f"{BASE_URL}/api/license/generate-demo-key",
            params={"plan": "pro", "days": 365},
            timeout=30,
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["plan"] == "pro"
        key = d["key"]
        assert isinstance(key, str) and "-" in key and len(key) >= 16
        state["pro_key"] = key

    def test_activate_pro(self, admin_session, state):
        key = state.get("pro_key")
        assert key, "need pro_key from previous test"
        r = admin_session.post(
            f"{BASE_URL}/api/license/activate",
            json={"key": key, "owner_name": "TEST_Owner"},
            timeout=30,
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["plan"] == "pro"
        assert d["days_left"] >= 364

        # GET to verify persistence
        r2 = admin_session.get(f"{BASE_URL}/api/license/status", timeout=30)
        assert r2.status_code == 200
        d2 = r2.json()
        assert d2["plan"] == "pro"
        assert d2["days_left"] >= 364
        # pro features should reflect
        assert d2["features"].get("ecommerce") is True
        assert d2["features"].get("portal") is True

    def test_activate_invalid_key(self, admin_session):
        r = admin_session.post(
            f"{BASE_URL}/api/license/activate",
            json={"key": "INVALID-KEY"},
            timeout=30,
        )
        assert r.status_code == 400, r.text
        msg = (r.json().get("detail") or "").lower()
        assert "invalid" in msg or "tampered" in msg

    def test_device_register_first(self, admin_session, state):
        fp = f"test-fp-{uuid.uuid4().hex[:8]}"
        state["device_fp"] = fp
        r = admin_session.post(
            f"{BASE_URL}/api/license/devices/register",
            json={"fingerprint": fp, "name": "TEST_Device"},
            timeout=30,
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["new"] is True
        assert d.get("device_id")
        state["device_id"] = d["device_id"]

    def test_device_register_duplicate(self, admin_session, state):
        fp = state["device_fp"]
        r = admin_session.post(
            f"{BASE_URL}/api/license/devices/register",
            json={"fingerprint": fp, "name": "TEST_Device"},
            timeout=30,
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["new"] is False

    def test_list_devices(self, admin_session, state):
        r = admin_session.get(f"{BASE_URL}/api/license/devices", timeout=30)
        assert r.status_code == 200
        d = r.json()
        ids = [dev["id"] for dev in d["devices"]]
        assert state["device_id"] in ids

    def test_delete_device(self, admin_session, state):
        did = state["device_id"]
        r = admin_session.delete(f"{BASE_URL}/api/license/devices/{did}", timeout=30)
        assert r.status_code == 200, r.text
        # verify removal
        r2 = admin_session.get(f"{BASE_URL}/api/license/devices", timeout=30)
        ids = [dev["id"] for dev in r2.json()["devices"]]
        assert did not in ids

    def test_start_trial_conflict(self, admin_session):
        r = admin_session.post(f"{BASE_URL}/api/license/start-trial", timeout=30)
        # already used by auto-init or pro activation
        assert r.status_code == 409, r.text

    def test_restore_trial_state(self, admin_session, state):
        """Cleanup: roll back to trial by generating + activating a fresh trial key."""
        r = admin_session.post(
            f"{BASE_URL}/api/license/generate-demo-key",
            params={"plan": "trial", "days": 30},
            timeout=30,
        )
        assert r.status_code == 200
        key = r.json()["key"]
        r2 = admin_session.post(
            f"{BASE_URL}/api/license/activate",
            json={"key": key, "owner_name": "RBS REGAL Trial"},
            timeout=30,
        )
        assert r2.status_code == 200


# =========================================================================
# Customer / Dealer Portal
# =========================================================================
TEST_PHONE = "9876" + str(int(time.time()))[-6:]  # unique per run, avoids duplicates from prior runs
TEST_NAME = "TEST_Portal_Customer"


class TestPortal:
    """Portal login flow + dashboard + order."""

    def test_login_no_party(self):
        r = requests.post(
            f"{BASE_URL}/api/portal/login",
            json={"phone": TEST_PHONE},
            timeout=30,
        )
        # Either 404 (clean DB) or 200 (party already exists). Accept either.
        if r.status_code == 200:
            pytest.skip("Party with this phone already exists — skipping 404 negative case")
        assert r.status_code == 404, r.text

    def test_create_party_then_login(self, admin_session, default_company_id, state):
        # First, clean any pre-existing test party with that phone
        listr = admin_session.get(
            f"{BASE_URL}/api/parties",
            params={"company_id": default_company_id},
            timeout=30,
        )
        if listr.status_code == 200:
            for p in listr.json():
                if p.get("phone") == TEST_PHONE and p.get("name", "").startswith("TEST_"):
                    admin_session.delete(f"{BASE_URL}/api/parties/{p['id']}", timeout=15)

        r = admin_session.post(
            f"{BASE_URL}/api/parties",
            params={"company_id": default_company_id},
            json={
                "name": TEST_NAME,
                "phone": TEST_PHONE,
                "type": "customer",
            },
            timeout=30,
        )
        assert r.status_code in (200, 201), r.text
        body = r.json()
        party_id = body.get("id") or body.get("_id")
        state["party_id"] = party_id
        state["company_id"] = default_company_id

        # Portal login with phone
        portal = requests.Session()
        lr = portal.post(
            f"{BASE_URL}/api/portal/login",
            json={"phone": TEST_PHONE, "company_id": default_company_id},
            timeout=30,
        )
        assert lr.status_code == 200, lr.text
        ld = lr.json()
        assert ld["ok"] is True
        assert ld["type"] == "customer"
        # token also returned for non-cookie testing
        state["portal_token"] = ld["token"]
        state["portal_session"] = portal

        # /me with cookie session
        mer = portal.get(f"{BASE_URL}/api/portal/me", timeout=30)
        assert mer.status_code == 200, mer.text
        md = mer.json()
        assert md["party_id"] == party_id
        assert md["type"] == "customer"

    def test_portal_dashboard(self, state):
        portal = state["portal_session"]
        r = portal.get(f"{BASE_URL}/api/portal/dashboard", timeout=30)
        assert r.status_code == 200, r.text
        d = r.json()
        assert "party" in d and "kpi" in d
        assert isinstance(d.get("invoices"), list)
        assert isinstance(d.get("payments"), list)
        for k in ("total_billed", "total_paid", "outstanding", "invoice_count"):
            assert k in d["kpi"]

    def test_portal_order(self, admin_session, state):
        portal = state["portal_session"]
        payload = {
            "lines": [
                {"name": "TEST_Item_Portal", "qty": 2, "rate": 100, "gst_rate": 18, "unit": "PCS"}
            ],
            "delivery_address": "Test Address",
            "notes": "TEST portal order",
        }
        r = portal.post(f"{BASE_URL}/api/portal/order", json=payload, timeout=30)
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["ok"] is True
        assert d["invoice_no"].startswith("SO/")
        # 2 * 100 = 200 + 18% = 236
        assert d["total"] == 236.0
        state["portal_order_id"] = d["order_id"]
        state["portal_invoice_no"] = d["invoice_no"]

        # Verify appears in /api/invoices with source=portal
        time.sleep(0.5)
        rr = admin_session.get(
            f"{BASE_URL}/api/invoices",
            params={"company_id": state["company_id"], "type": "sale_order"},
            timeout=30,
        )
        assert rr.status_code == 200
        found = [i for i in rr.json() if i.get("invoice_no") == d["invoice_no"]]
        assert found, f"order {d['invoice_no']} not visible to admin"
        assert found[0].get("source") == "portal"


# =========================================================================
# Public Storefront + Invoice Verify
# =========================================================================
STORE_PHONE = "9988776655"


class TestStorefront:
    def test_items_public(self, default_company_id, state):
        state["company_id"] = default_company_id
        r = requests.get(
            f"{BASE_URL}/api/storefront/{default_company_id}/items",
            timeout=30,
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["company"]["id"] == default_company_id
        assert isinstance(d["items"], list)

    def test_guest_order_creates_party(self, admin_session, default_company_id, state):
        # Clean any pre-existing party with that phone
        lr = admin_session.get(
            f"{BASE_URL}/api/parties",
            params={"company_id": default_company_id},
            timeout=30,
        )
        if lr.status_code == 200:
            for p in lr.json():
                if p.get("phone") == STORE_PHONE:
                    admin_session.delete(f"{BASE_URL}/api/parties/{p['id']}", timeout=15)

        payload = {
            "customer_name": "TEST_Guest_Customer",
            "customer_phone": STORE_PHONE,
            "customer_address": "Test Shipping Addr",
            "lines": [
                {"name": "TEST_Storefront_Item", "qty": 1, "rate": 250, "gst_rate": 0, "unit": "PCS"}
            ],
            "notes": "TEST guest order",
        }
        r = requests.post(
            f"{BASE_URL}/api/storefront/{default_company_id}/order",
            json=payload,
            timeout=30,
        )
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["ok"] is True
        assert d["total"] == 250.0
        state["storefront_party_id"] = d["party_id"]
        state["storefront_order_id"] = d["order_id"]
        state["storefront_invoice_no"] = d["invoice_no"]

        # Verify party appears in /api/parties
        lr = admin_session.get(
            f"{BASE_URL}/api/parties",
            params={"company_id": default_company_id},
            timeout=30,
        )
        assert lr.status_code == 200
        ids = [p["id"] for p in lr.json()]
        assert d["party_id"] in ids

    def test_verify_invoice_public(self, state):
        oid = state.get("storefront_order_id")
        assert oid
        r = requests.get(f"{BASE_URL}/api/verify/{oid}", timeout=30)
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["verified"] is True
        assert d["invoice_no"] == state["storefront_invoice_no"]
        assert d["total"] == 250.0
        assert "company" in d and d["company"].get("name")


# =========================================================================
# Cleanup
# =========================================================================
class TestZZCleanup:
    """Runs alphabetically last — deletes all test data created above."""

    def test_cleanup(self, admin_session, state):
        # Delete portal sale_order invoice
        for key in ("portal_order_id", "storefront_order_id"):
            oid = state.get(key)
            if oid:
                admin_session.delete(f"{BASE_URL}/api/invoices/{oid}", timeout=15)
        # Delete parties
        for key in ("party_id", "storefront_party_id"):
            pid = state.get(key)
            if pid:
                admin_session.delete(f"{BASE_URL}/api/parties/{pid}", timeout=15)
        # Devices already deleted in licensing test
        # License already restored to trial in test_restore_trial_state
        assert True
