"""Comprehensive backend tests for RBS Regal Business Suite."""
import uuid
import pytest
import requests

from conftest import BASE_URL, ADMIN_EMAIL, ADMIN_PASSWORD, STAFF_EMAIL, STAFF_PASSWORD, _new_session, _login


# ======================== AUTH ========================
class TestAuth:
    def test_health(self):
        r = requests.get(f"{BASE_URL}/api/", timeout=30)
        assert r.status_code == 200
        assert r.json().get("status") == "ok"

    def test_admin_login_success_sets_cookies(self):
        s = _new_session()
        r = _login(s, ADMIN_EMAIL, ADMIN_PASSWORD)
        assert r.status_code == 200, r.text
        data = r.json()
        assert data["email"] == ADMIN_EMAIL
        assert data["role"] == "admin"
        assert "id" in data
        # Cookies must be set
        cookie_names = {c.name for c in s.cookies}
        assert "access_token" in cookie_names
        assert "refresh_token" in cookie_names

    def test_me_returns_admin(self, admin_session):
        r = admin_session.get(f"{BASE_URL}/api/auth/me", timeout=30)
        assert r.status_code == 200, r.text
        data = r.json()
        assert data["email"] == ADMIN_EMAIL
        assert data["role"] == "admin"

    def test_login_wrong_password_returns_401(self):
        s = _new_session()
        # Use a unique email to avoid affecting lockout state of real accounts
        r = s.post(f"{BASE_URL}/api/auth/login",
                   json={"email": ADMIN_EMAIL, "password": "WrongPassword!"}, timeout=30)
        assert r.status_code == 401, r.text

    def test_staff_login(self):
        s = _new_session()
        r = _login(s, STAFF_EMAIL, STAFF_PASSWORD)
        assert r.status_code == 200, r.text
        assert r.json()["role"] == "staff"

    def test_me_unauthenticated_returns_401(self):
        s = _new_session()
        r = s.get(f"{BASE_URL}/api/auth/me", timeout=30)
        assert r.status_code == 401


# ======================== COMPANIES ========================
class TestCompanies:
    def test_list_companies_default_seeded(self, admin_session):
        r = admin_session.get(f"{BASE_URL}/api/companies", timeout=30)
        assert r.status_code == 200
        companies = r.json()
        assert any("RBS Regal" in c.get("name", "") for c in companies)

    def test_create_update_delete_company_admin(self, admin_session):
        unique = f"TEST_Co_{uuid.uuid4().hex[:6]}"
        # Create
        r = admin_session.post(f"{BASE_URL}/api/companies",
                               json={"name": unique, "state": "Karnataka"}, timeout=30)
        assert r.status_code == 200, r.text
        cid = r.json()["id"]
        assert r.json()["name"] == unique

        # GET verify persistence
        r2 = admin_session.get(f"{BASE_URL}/api/companies", timeout=30)
        assert any(c["id"] == cid for c in r2.json())

        # Update
        r3 = admin_session.put(f"{BASE_URL}/api/companies/{cid}",
                               json={"name": unique + "_upd", "state": "Goa"}, timeout=30)
        assert r3.status_code == 200
        assert r3.json()["name"] == unique + "_upd"

        # Delete
        r4 = admin_session.delete(f"{BASE_URL}/api/companies/{cid}", timeout=30)
        assert r4.status_code == 200

    def test_create_company_staff_forbidden(self, staff_session):
        r = staff_session.post(f"{BASE_URL}/api/companies",
                               json={"name": "TEST_StaffCo"}, timeout=30)
        assert r.status_code == 403, r.text


# ======================== PARTIES ========================
class TestParties:
    def test_create_and_list_party(self, admin_session, default_company_id):
        name = f"TEST_Party_{uuid.uuid4().hex[:6]}"
        r = admin_session.post(
            f"{BASE_URL}/api/parties?company_id={default_company_id}",
            json={"name": name, "type": "customer", "gstin": "27ABCDE1234F1Z5"}, timeout=30)
        assert r.status_code == 200, r.text
        pid = r.json()["id"]
        assert r.json()["name"] == name

        r2 = admin_session.get(f"{BASE_URL}/api/parties?company_id={default_company_id}", timeout=30)
        assert r2.status_code == 200
        found = [p for p in r2.json() if p["id"] == pid]
        assert found
        assert found[0].get("outstanding") == 0

    def test_create_party_staff_forbidden(self, staff_session, default_company_id):
        r = staff_session.post(
            f"{BASE_URL}/api/parties?company_id={default_company_id}",
            json={"name": "TEST_StaffParty", "type": "customer"}, timeout=30)
        assert r.status_code == 403


# ======================== ITEMS ========================
class TestItems:
    def test_create_and_list_item(self, admin_session, default_company_id):
        name = f"TEST_Item_{uuid.uuid4().hex[:6]}"
        r = admin_session.post(
            f"{BASE_URL}/api/items?company_id={default_company_id}",
            json={"name": name, "hsn": "1234", "gst_rate": 18.0,
                  "sale_price": 100.0, "purchase_price": 80.0,
                  "opening_stock": 50, "current_stock": 50,
                  "low_stock_threshold": 5}, timeout=30)
        assert r.status_code == 200, r.text
        iid = r.json()["id"]
        assert r.json()["name"] == name
        assert r.json()["gst_rate"] == 18.0
        assert r.json()["current_stock"] == 50

        r2 = admin_session.get(f"{BASE_URL}/api/items?company_id={default_company_id}", timeout=30)
        assert any(it["id"] == iid for it in r2.json())

    def test_create_item_staff_forbidden(self, staff_session, default_company_id):
        r = staff_session.post(
            f"{BASE_URL}/api/items?company_id={default_company_id}",
            json={"name": "TEST_StaffItem"}, timeout=30)
        assert r.status_code == 403


# ======================== INVOICES ========================
@pytest.fixture(scope="class")
def seeded_item(admin_session, default_company_id):
    """Create a dedicated item for invoice tests."""
    name = f"TEST_InvItem_{uuid.uuid4().hex[:6]}"
    r = admin_session.post(
        f"{BASE_URL}/api/items?company_id={default_company_id}",
        json={"name": name, "hsn": "9999", "gst_rate": 18.0,
              "sale_price": 100.0, "purchase_price": 60.0,
              "opening_stock": 100, "current_stock": 100,
              "low_stock_threshold": 5}, timeout=30)
    assert r.status_code == 200, r.text
    return r.json()


class TestInvoices:
    def test_create_sale_invoice_calc_and_stock(self, admin_session, default_company_id, seeded_item):
        iid = seeded_item["id"]
        stock_before = float(seeded_item["current_stock"])

        payload = {
            "type": "sale",
            "party_name": "TEST_WalkIn",
            "lines": [
                {"item_id": iid, "name": seeded_item["name"], "qty": 2, "rate": 100.0,
                 "discount": 0.0, "gst_rate": 18.0, "hsn": "9999"},
                {"item_id": iid, "name": seeded_item["name"], "qty": 1, "rate": 200.0,
                 "discount": 10.0, "gst_rate": 18.0, "hsn": "9999"},
            ],
            "payment_received": 0,
        }
        r = admin_session.post(
            f"{BASE_URL}/api/invoices?company_id={default_company_id}",
            json=payload, timeout=30)
        assert r.status_code == 200, r.text
        inv = r.json()
        # subtotal = 2*100 + (1*200 - 10%) = 200 + 180 = 380
        # total_gst = 380 * 0.18 = 68.40
        # cgst = sgst = 34.20
        # total = 448.40
        assert abs(inv["subtotal"] - 380.0) < 0.01
        assert abs(inv["total_gst"] - 68.40) < 0.05
        assert abs(inv["cgst"] - 34.20) < 0.05
        assert abs(inv["sgst"] - 34.20) < 0.05
        assert abs(inv["total"] - 448.40) < 0.05
        # Invoice no pattern INV/YY/00001
        assert inv["invoice_no"].startswith("INV/")
        parts = inv["invoice_no"].split("/")
        assert len(parts) == 3 and len(parts[2]) == 5

        # Stock decremented by 3
        r2 = admin_session.get(f"{BASE_URL}/api/items?company_id={default_company_id}", timeout=30)
        upd = next(it for it in r2.json() if it["id"] == iid)
        assert float(upd["current_stock"]) == stock_before - 3, f"expected {stock_before-3}, got {upd['current_stock']}"

        # Save for next test
        TestInvoices.last_sale_id = inv["id"]
        TestInvoices.last_sale_no = inv["invoice_no"]

    def test_create_purchase_increments_stock(self, admin_session, default_company_id, seeded_item):
        iid = seeded_item["id"]
        r0 = admin_session.get(f"{BASE_URL}/api/items?company_id={default_company_id}", timeout=30)
        stock_before = float(next(it for it in r0.json() if it["id"] == iid)["current_stock"])

        payload = {
            "type": "purchase",
            "party_name": "TEST_Vendor",
            "lines": [{"item_id": iid, "name": seeded_item["name"], "qty": 5,
                       "rate": 60.0, "discount": 0.0, "gst_rate": 18.0, "hsn": "9999"}],
        }
        r = admin_session.post(
            f"{BASE_URL}/api/invoices?company_id={default_company_id}",
            json=payload, timeout=30)
        assert r.status_code == 200, r.text
        assert r.json()["invoice_no"].startswith("PUR/")

        r2 = admin_session.get(f"{BASE_URL}/api/items?company_id={default_company_id}", timeout=30)
        stock_after = float(next(it for it in r2.json() if it["id"] == iid)["current_stock"])
        assert stock_after == stock_before + 5

    def test_list_sale_invoices(self, admin_session, default_company_id):
        r = admin_session.get(
            f"{BASE_URL}/api/invoices?company_id={default_company_id}&type=sale", timeout=30)
        assert r.status_code == 200
        assert all(inv["type"] == "sale" for inv in r.json())

    def test_list_purchase_invoices(self, admin_session, default_company_id):
        r = admin_session.get(
            f"{BASE_URL}/api/invoices?company_id={default_company_id}&type=purchase", timeout=30)
        assert r.status_code == 200
        assert all(inv["type"] == "purchase" for inv in r.json())

    def test_get_invoice_by_id_has_lines(self, admin_session):
        inv_id = getattr(TestInvoices, "last_sale_id", None)
        if not inv_id:
            pytest.skip("No sale invoice id available")
        r = admin_session.get(f"{BASE_URL}/api/invoices/{inv_id}", timeout=30)
        assert r.status_code == 200
        data = r.json()
        assert data["id"] == inv_id
        assert "lines" in data and len(data["lines"]) >= 1
        assert "total" in data

    def test_soft_delete_and_restore(self, admin_session, default_company_id):
        inv_id = getattr(TestInvoices, "last_sale_id", None)
        if not inv_id:
            pytest.skip("No sale invoice id available")
        # Delete (soft)
        r = admin_session.delete(f"{BASE_URL}/api/invoices/{inv_id}", timeout=30)
        assert r.status_code == 200

        # Active list should not include it
        r2 = admin_session.get(f"{BASE_URL}/api/invoices?company_id={default_company_id}", timeout=30)
        assert not any(inv["id"] == inv_id for inv in r2.json())

        # Trash should include it
        r3 = admin_session.get(f"{BASE_URL}/api/invoices-trash?company_id={default_company_id}", timeout=30)
        assert r3.status_code == 200
        assert any(inv["id"] == inv_id for inv in r3.json())

        # Restore
        r4 = admin_session.post(f"{BASE_URL}/api/invoices/{inv_id}/restore", timeout=30)
        assert r4.status_code == 200

        # Should be back in active list
        r5 = admin_session.get(f"{BASE_URL}/api/invoices?company_id={default_company_id}", timeout=30)
        assert any(inv["id"] == inv_id for inv in r5.json())

    def test_staff_cannot_delete_invoice(self, staff_session, admin_session, default_company_id, seeded_item):
        # Create a quick invoice
        payload = {"type": "sale", "party_name": "TEST_StaffDel",
                   "lines": [{"item_id": seeded_item["id"], "name": seeded_item["name"],
                              "qty": 1, "rate": 50.0, "gst_rate": 18.0}]}
        r = admin_session.post(
            f"{BASE_URL}/api/invoices?company_id={default_company_id}",
            json=payload, timeout=30)
        assert r.status_code == 200
        iid = r.json()["id"]
        rd = staff_session.delete(f"{BASE_URL}/api/invoices/{iid}", timeout=30)
        assert rd.status_code == 403


# ======================== EXPENSES ========================
class TestExpenses:
    def test_create_list_delete_expense(self, admin_session, default_company_id):
        r = admin_session.post(
            f"{BASE_URL}/api/expenses?company_id={default_company_id}",
            json={"category": "TEST_Rent", "amount": 5000.0, "vendor": "TEST_Landlord"}, timeout=30)
        assert r.status_code == 200, r.text
        eid = r.json()["id"]
        assert r.json()["amount"] == 5000.0

        r2 = admin_session.get(f"{BASE_URL}/api/expenses?company_id={default_company_id}", timeout=30)
        assert any(e["id"] == eid for e in r2.json())

        r3 = admin_session.delete(f"{BASE_URL}/api/expenses/{eid}", timeout=30)
        assert r3.status_code == 200


# ======================== DASHBOARD & REPORTS ========================
class TestDashboardReports:
    def test_dashboard_stats_shape(self, admin_session, default_company_id):
        r = admin_session.get(f"{BASE_URL}/api/dashboard/stats?company_id={default_company_id}", timeout=30)
        assert r.status_code == 200, r.text
        d = r.json()
        for key in ["total_sales", "total_purchases", "total_expenses", "receivable",
                    "stock_value", "profit", "low_stock", "sales_series", "top_items", "recent_sales"]:
            assert key in d, f"missing {key}"
        assert isinstance(d["low_stock"], list)
        assert isinstance(d["sales_series"], list)
        assert isinstance(d["top_items"], list)

    def test_gst_report(self, admin_session, default_company_id):
        r = admin_session.get(f"{BASE_URL}/api/reports/gst?company_id={default_company_id}", timeout=30)
        assert r.status_code == 200
        rows = r.json()["rows"]
        if rows:
            row = rows[0]
            for key in ["rate", "taxable", "gst", "cgst", "sgst"]:
                assert key in row

    def test_sales_summary(self, admin_session, default_company_id):
        r = admin_session.get(f"{BASE_URL}/api/reports/sales-summary?company_id={default_company_id}", timeout=30)
        assert r.status_code == 200
        assert "rows" in r.json()

    def test_stock_report(self, admin_session, default_company_id):
        r = admin_session.get(f"{BASE_URL}/api/reports/stock?company_id={default_company_id}", timeout=30)
        assert r.status_code == 200
        rows = r.json()["rows"]
        if rows:
            assert "value" in rows[0]


# ======================== USERS ========================
class TestUsers:
    def test_list_users_no_password_hash(self, admin_session):
        r = admin_session.get(f"{BASE_URL}/api/users", timeout=30)
        assert r.status_code == 200
        users = r.json()
        for u in users:
            assert "password_hash" not in u

    def test_staff_cannot_list_users(self, staff_session):
        r = staff_session.get(f"{BASE_URL}/api/users", timeout=30)
        assert r.status_code == 403

    def test_create_update_delete_user(self, admin_session):
        email = f"test_user_{uuid.uuid4().hex[:6]}@example.com"
        # Create
        r = admin_session.post(f"{BASE_URL}/api/users",
                               json={"email": email, "password": "Pass@1234",
                                     "name": "TEST User", "role": "staff"}, timeout=30)
        assert r.status_code == 200, r.text
        uid = r.json()["id"]
        assert r.json()["email"] == email
        assert "password_hash" not in r.json()

        # Update
        r2 = admin_session.put(f"{BASE_URL}/api/users/{uid}",
                               json={"name": "TEST Updated", "password": "Pass@5678"}, timeout=30)
        assert r2.status_code == 200
        assert r2.json()["name"] == "TEST Updated"

        # Verify new password works
        s = _new_session()
        rl = _login(s, email, "Pass@5678")
        assert rl.status_code == 200

        # Delete
        r3 = admin_session.delete(f"{BASE_URL}/api/users/{uid}", timeout=30)
        assert r3.status_code == 200

    def test_cannot_delete_self(self, admin_session):
        me = admin_session.get(f"{BASE_URL}/api/auth/me", timeout=30).json()
        r = admin_session.delete(f"{BASE_URL}/api/users/{me['id']}", timeout=30)
        assert r.status_code == 400

    def test_staff_cannot_create_user(self, staff_session):
        r = staff_session.post(f"{BASE_URL}/api/users",
                               json={"email": "blockme@example.com", "password": "x",
                                     "name": "x", "role": "staff"}, timeout=30)
        assert r.status_code == 403


# ======================== ACTIVITY LOGS ========================
class TestActivityLogs:
    def test_admin_can_list(self, admin_session):
        r = admin_session.get(f"{BASE_URL}/api/activity-logs", timeout=30)
        assert r.status_code == 200
        logs = r.json()
        assert isinstance(logs, list)
        # We've done many actions; should have entries
        if logs:
            assert "action" in logs[0]
            assert "entity" in logs[0]

    def test_staff_blocked(self, staff_session):
        r = staff_session.get(f"{BASE_URL}/api/activity-logs", timeout=30)
        assert r.status_code == 403
