"""Backend tests for /api/shortcuts/* — user-customisable keyboard shortcut overrides.

Covers:
  * Auth required (anonymous 401)
  * Empty list for fresh user
  * Upsert (PUT) with conflict detection
  * Disabled flag bypasses conflict check
  * DELETE removes single override
  * POST /reset-all clears all
  * GET /logs returns history with expected fields
"""
import os
import requests
import pytest

BASE_URL = os.environ.get("REACT_APP_BACKEND_URL", "https://offline-billing-pro-2.preview.emergentagent.com").rstrip("/")


# ---------- module helpers ----------

def _cleanup(session):
    """Best-effort reset for the active user."""
    try:
        session.post(f"{BASE_URL}/api/shortcuts/me/reset-all", timeout=15)
    except Exception:
        pass


@pytest.fixture(scope="module", autouse=True)
def _reset_before_and_after(admin_session, staff_session):
    _cleanup(admin_session)
    _cleanup(staff_session)
    yield
    _cleanup(admin_session)
    _cleanup(staff_session)


# ---------- auth gate ----------

class TestAuthGate:
    def test_anonymous_me_returns_401(self):
        r = requests.get(f"{BASE_URL}/api/shortcuts/me", timeout=15)
        assert r.status_code == 401, f"Expected 401, got {r.status_code}: {r.text[:200]}"

    def test_anonymous_logs_returns_401(self):
        r = requests.get(f"{BASE_URL}/api/shortcuts/logs", timeout=15)
        assert r.status_code == 401

    def test_anonymous_put_returns_401(self):
        r = requests.put(f"{BASE_URL}/api/shortcuts/me/nav.home", json={"keys": "Shift+G"}, timeout=15)
        assert r.status_code == 401


# ---------- CRUD flow ----------

class TestShortcutCRUD:
    def test_fresh_user_returns_empty_dict(self, admin_session):
        # Already cleaned up by fixture
        r = admin_session.get(f"{BASE_URL}/api/shortcuts/me", timeout=15)
        assert r.status_code == 200, r.text
        data = r.json()
        assert isinstance(data, dict)
        assert data == {}, f"Expected empty dict for fresh user, got {data}"

    def test_put_saves_override_and_persists(self, admin_session):
        r = admin_session.put(
            f"{BASE_URL}/api/shortcuts/me/nav.home",
            json={"keys": "Shift+G", "disabled": False},
            timeout=15,
        )
        assert r.status_code == 200, r.text
        body = r.json()
        assert body.get("ok") is True
        sc = body.get("shortcut", {})
        assert sc.get("keys") == "Shift+G"
        assert sc.get("disabled") is False
        assert sc.get("shortcut_id") == "nav.home"
        # _id must NOT leak
        assert "_id" not in sc

        # GET to verify persistence
        r2 = admin_session.get(f"{BASE_URL}/api/shortcuts/me", timeout=15)
        assert r2.status_code == 200
        m = r2.json()
        assert "nav.home" in m
        assert m["nav.home"]["keys"] == "Shift+G"
        assert m["nav.home"]["disabled"] is False
        assert "_id" not in m["nav.home"]

    def test_put_conflict_returns_409(self, admin_session):
        # nav.home already on Shift+G from previous test — now try nav.parties → Shift+G
        r = admin_session.put(
            f"{BASE_URL}/api/shortcuts/me/nav.parties",
            json={"keys": "Shift+G", "disabled": False},
            timeout=15,
        )
        assert r.status_code == 409, f"Expected 409 conflict, got {r.status_code}: {r.text[:200]}"
        # error message mentions the conflicting shortcut
        assert "nav.home" in r.text or "Conflict" in r.text

    def test_put_disabled_skips_conflict_check(self, admin_session):
        # Saving the same keys with disabled=true must NOT 409
        r = admin_session.put(
            f"{BASE_URL}/api/shortcuts/me/nav.parties",
            json={"keys": "Shift+G", "disabled": True},
            timeout=15,
        )
        assert r.status_code == 200, r.text
        assert r.json().get("shortcut", {}).get("disabled") is True

    def test_put_update_changes_keys(self, admin_session):
        # Change nav.parties to different keys (also flipping disabled false)
        r = admin_session.put(
            f"{BASE_URL}/api/shortcuts/me/nav.parties",
            json={"keys": "Shift+J", "disabled": False},
            timeout=15,
        )
        assert r.status_code == 200, r.text
        sc = r.json()["shortcut"]
        assert sc["keys"] == "Shift+J"
        assert sc["disabled"] is False

    def test_delete_resets_single_override(self, admin_session):
        r = admin_session.delete(f"{BASE_URL}/api/shortcuts/me/nav.parties", timeout=15)
        assert r.status_code == 200, r.text
        body = r.json()
        assert body.get("ok") is True
        assert body.get("reset") is True

        # Verify gone
        r2 = admin_session.get(f"{BASE_URL}/api/shortcuts/me", timeout=15)
        assert r2.status_code == 200
        assert "nav.parties" not in r2.json()

    def test_delete_nonexistent_is_idempotent(self, admin_session):
        r = admin_session.delete(f"{BASE_URL}/api/shortcuts/me/nav.does-not-exist", timeout=15)
        assert r.status_code == 200, r.text
        body = r.json()
        assert body.get("ok") is True
        assert body.get("reset") is False

    def test_reset_all_clears_overrides(self, admin_session):
        # Ensure at least 2 overrides exist
        admin_session.put(f"{BASE_URL}/api/shortcuts/me/nav.items", json={"keys": "Shift+I"}, timeout=15)
        admin_session.put(f"{BASE_URL}/api/shortcuts/me/nav.sales", json={"keys": "Shift+L"}, timeout=15)
        pre = admin_session.get(f"{BASE_URL}/api/shortcuts/me", timeout=15).json()
        assert len(pre) >= 2

        r = admin_session.post(f"{BASE_URL}/api/shortcuts/me/reset-all", timeout=15)
        assert r.status_code == 200, r.text
        body = r.json()
        assert body.get("ok") is True
        assert body.get("removed", 0) >= 2

        post = admin_session.get(f"{BASE_URL}/api/shortcuts/me", timeout=15).json()
        assert post == {}


# ---------- logs ----------

class TestShortcutLogs:
    def test_logs_contain_expected_fields(self, admin_session):
        # Generate some activity
        admin_session.put(f"{BASE_URL}/api/shortcuts/me/nav.items", json={"keys": "Shift+K"}, timeout=15)
        admin_session.delete(f"{BASE_URL}/api/shortcuts/me/nav.items", timeout=15)

        r = admin_session.get(f"{BASE_URL}/api/shortcuts/logs", timeout=15)
        assert r.status_code == 200, r.text
        logs = r.json()
        assert isinstance(logs, list)
        assert len(logs) >= 2, f"Expected at least 2 log entries, got {len(logs)}"

        # First entry (most recent — reset)
        sample = logs[0]
        assert "_id" not in sample
        assert "action" in sample
        assert "ts" in sample
        assert sample["action"] in {"update", "reset", "reset-all"}

        # Confirm an update entry has old/new keys
        updates = [row for row in logs if row.get("action") == "update" and row.get("shortcut_id") == "nav.items"]
        assert updates, "Expected at least one 'update' log for nav.items"
        assert "new_keys" in updates[-1]


# ---------- staff user also works ----------

class TestStaffAccess:
    def test_staff_can_manage_own_overrides(self, staff_session):
        r = staff_session.put(
            f"{BASE_URL}/api/shortcuts/me/nav.home",
            json={"keys": "Shift+B"},
            timeout=15,
        )
        assert r.status_code == 200, r.text
        r2 = staff_session.get(f"{BASE_URL}/api/shortcuts/me", timeout=15)
        assert r2.status_code == 200
        assert r2.json().get("nav.home", {}).get("keys") == "Shift+B"


# ---------- regression: items bulk + import/export still alive ----------

class TestItemsRegression:
    def test_items_list_ok(self, admin_session, default_company_id):
        r = admin_session.get(f"{BASE_URL}/api/items?company_id={default_company_id}", timeout=30)
        assert r.status_code == 200, r.text
        assert isinstance(r.json(), list)

    def test_items_import_template_ok(self, admin_session):
        r = admin_session.get(f"{BASE_URL}/api/items/import/template", timeout=30)
        # Template might be CSV/XLSX or json — accept any 200
        assert r.status_code == 200, f"{r.status_code} {r.text[:200]}"
        assert len(r.content) > 0

    def test_items_export_ok(self, admin_session, default_company_id):
        r = admin_session.get(f"{BASE_URL}/api/items/export?company_id={default_company_id}", timeout=30)
        assert r.status_code == 200, f"{r.status_code} {r.text[:200]}"
        assert len(r.content) > 0

    def test_items_bulk_update_noop_ok(self, admin_session, default_company_id):
        # Pick any item and apply a no-op patch (set sale_price to its current value or a small change reverted)
        items = admin_session.get(f"{BASE_URL}/api/items?company_id={default_company_id}", timeout=30).json()
        if not items:
            pytest.skip("No items in default company to test bulk-update")
        target = items[0]
        original_price = target.get("sale_price", 0)
        r = admin_session.post(
            f"{BASE_URL}/api/items/bulk-update?company_id={default_company_id}",
            json={"updates": [{"id": target["id"], "patch": {"sale_price": original_price}}]},
            timeout=30,
        )
        assert r.status_code == 200, f"{r.status_code} {r.text[:200]}"
        body = r.json()
        assert body.get("ok") is True
        assert body.get("updated") == 1
