"""End-to-end tests for the Access Control + Payroll modules (v12.13)."""
import time

import pytest

from conftest import BASE_URL  # type: ignore[import-not-found]


@pytest.fixture(scope="module")
def admin_user_id(admin_session):
    r = admin_session.get(f"{BASE_URL}/api/me/permissions", timeout=20)
    assert r.status_code == 200, r.text
    return r.json()["user_id"]


@pytest.mark.critical
class TestPermissionsSnapshot:
    def test_me_permissions_shape(self, admin_session):
        r = admin_session.get(f"{BASE_URL}/api/me/permissions", timeout=20)
        assert r.status_code == 200, r.text
        body = r.json()
        assert "modules" in body
        assert "payroll_actions" in body
        assert "module_catalogue" in body
        assert "payroll_catalogue" in body
        assert "payroll" in body["module_catalogue"]
        assert "view" in body["payroll_catalogue"]
        assert "salary_visible" in body["payroll_catalogue"]


@pytest.mark.critical
class TestAdminAccessEndpoints:
    def test_list_users_includes_resolved_perms(self, admin_session):
        r = admin_session.get(f"{BASE_URL}/api/admin/access/users", timeout=20)
        assert r.status_code == 200, r.text
        body = r.json()
        assert "users" in body and len(body["users"]) >= 1
        for u in body["users"]:
            assert "modules" in u and "payroll" in u["modules"]
            assert "payroll_actions" in u and "view" in u["payroll_actions"]

    def test_set_user_modules_creates_audit_entry(self, admin_session, admin_user_id):
        before = admin_session.get(f"{BASE_URL}/api/me/permissions", timeout=20).json()
        old_payroll = before["modules"].get("payroll", False)
        new_val = not old_payroll
        r = admin_session.put(
            f"{BASE_URL}/api/admin/access/user/{admin_user_id}/modules",
            json={"modules": {"payroll": new_val}},
            timeout=20,
        )
        assert r.status_code == 200, r.text
        assert r.json()["modules"]["payroll"] == new_val
        audit = admin_session.get(
            f"{BASE_URL}/api/admin/access/audit",
            params={"target_user_id": admin_user_id, "limit": 10},
            timeout=20,
        )
        assert audit.status_code == 200, audit.text
        rows = audit.json()
        assert any(r2["scope"] == "module" and r2["key"] == "payroll" for r2 in rows)
        # Roll back
        admin_session.put(
            f"{BASE_URL}/api/admin/access/user/{admin_user_id}/modules",
            json={"modules": {"payroll": old_payroll}},
            timeout=20,
        )

    def test_self_cannot_revoke_own_admin_panel(self, admin_session, admin_user_id):
        r = admin_session.put(
            f"{BASE_URL}/api/admin/access/user/{admin_user_id}/modules",
            json={"modules": {"admin_panel": False}},
            timeout=20,
        )
        assert r.status_code in (200, 409), r.text


@pytest.mark.critical
class TestPayrollEndpoints:
    def test_payroll_view_unauthorized_for_default_admin(self, admin_session, default_company_id):
        """Admin role without explicit Payroll grant should get 403."""
        # Ensure NO grant first
        me = admin_session.get(f"{BASE_URL}/api/me/permissions", timeout=20).json()
        if not me.get("is_super_admin"):
            admin_session.put(
                f"{BASE_URL}/api/admin/access/user/{me['user_id']}/modules",
                json={"modules": {"payroll": False}},
                timeout=20,
            )
        r = admin_session.get(
            f"{BASE_URL}/api/payroll/employees",
            params={"company_id": default_company_id},
            timeout=20,
        )
        if me.get("is_super_admin"):
            assert r.status_code == 200, f"Super admin should always pass — got {r.status_code}: {r.text}"
        else:
            assert r.status_code == 403, f"Default admin should be denied — got {r.status_code}: {r.text}"

    def test_employee_crud_and_payroll_run(self, admin_session, default_company_id, admin_user_id):
        # Grant self full payroll access
        admin_session.put(
            f"{BASE_URL}/api/admin/access/user/{admin_user_id}/modules",
            json={"modules": {"payroll": True}},
            timeout=20,
        )
        admin_session.put(
            f"{BASE_URL}/api/admin/access/user/{admin_user_id}/payroll-permissions",
            json={"perms": {
                "view": True, "create": True, "edit": True, "delete": True,
                "approve": True, "generate_payslip": True, "export": True,
                "bulk": True, "salary_visible": True,
            }},
            timeout=20,
        )
        unique = f"PayTest_{int(time.time()*1000)}"
        r = admin_session.post(
            f"{BASE_URL}/api/payroll/employees",
            params={"company_id": default_company_id},
            json={
                "name": unique, "code": "EMP01", "designation": "Tester",
                "basic": 18000, "hra": 8000, "allowances": 2000,
                "pf_applicable": True, "esi_applicable": True, "pt_applicable": True,
                "is_active": True,
            },
            timeout=30,
        )
        assert r.status_code == 200, r.text
        emp = r.json()
        assert emp["name"] == unique
        emp_id = emp["id"]

        # Create payroll run
        month_tag = f"2026-{(int(time.time()) % 9) + 1:02d}"
        r_run = admin_session.post(
            f"{BASE_URL}/api/payroll/runs",
            json={"month": month_tag, "company_id": default_company_id, "notes": "auto-test"},
            timeout=30,
        )
        if r_run.status_code == 409:
            # rare collision — pick a different month
            month_tag = f"2026-{((int(time.time()) % 9) + 2):02d}"
            r_run = admin_session.post(
                f"{BASE_URL}/api/payroll/runs",
                json={"month": month_tag, "company_id": default_company_id, "notes": "auto-test"},
                timeout=30,
            )
        assert r_run.status_code == 200, r_run.text
        run = r_run.json()
        assert run["status"] == "draft"
        run_id = run["id"]

        # List payslips → check statutory math
        r_ps = admin_session.get(f"{BASE_URL}/api/payroll/runs/{run_id}/payslips", timeout=20)
        assert r_ps.status_code == 200, r_ps.text
        slips = r_ps.json()
        match = next((s for s in slips if s["employee_id"] == emp_id), None)
        assert match
        comp = match["computation"]
        # Gross=28000; PF=1800 cap; ESI=0; PT=200; Net=26000
        assert comp["pf"] == 1800.0
        assert comp["esi"] == 0.0
        assert comp["pt"] == 200.0
        assert comp["net_pay"] == 26000.0

        # Approve
        r_app = admin_session.post(f"{BASE_URL}/api/payroll/runs/{run_id}/approve", timeout=20)
        assert r_app.status_code == 200, r_app.text
        assert r_app.json()["status"] == "approved"

        # Cleanup
        admin_session.delete(f"{BASE_URL}/api/payroll/runs/{run_id}", timeout=20)
        admin_session.delete(f"{BASE_URL}/api/payroll/employees/{emp_id}", timeout=20)
        # Roll back grants
        admin_session.put(
            f"{BASE_URL}/api/admin/access/user/{admin_user_id}/modules",
            json={"modules": {"payroll": False}},
            timeout=20,
        )

    def test_salary_visibility_masks_amounts(self, admin_session, default_company_id, admin_user_id):
        """When salary_visible=False, numeric fields in computation should be masked."""
        # Grant view + create, but DENY salary_visible
        admin_session.put(
            f"{BASE_URL}/api/admin/access/user/{admin_user_id}/modules",
            json={"modules": {"payroll": True}},
            timeout=20,
        )
        admin_session.put(
            f"{BASE_URL}/api/admin/access/user/{admin_user_id}/payroll-permissions",
            json={"perms": {
                "view": True, "create": True, "approve": True,
                "salary_visible": False,  # MASKED
            }},
            timeout=20,
        )
        # Need an employee + run
        unique = f"MaskTest_{int(time.time()*1000)}"
        r_emp = admin_session.post(
            f"{BASE_URL}/api/payroll/employees",
            params={"company_id": default_company_id},
            json={"name": unique, "basic": 20000, "hra": 5000, "is_active": True},
            timeout=30,
        )
        assert r_emp.status_code == 200, r_emp.text
        emp_id = r_emp.json()["id"]
        # Run
        month_tag = f"2027-{(int(time.time()) % 9) + 1:02d}"
        r_run = admin_session.post(
            f"{BASE_URL}/api/payroll/runs",
            json={"month": month_tag, "company_id": default_company_id},
            timeout=30,
        )
        # If month collision skip the masking check
        if r_run.status_code != 200:
            admin_session.delete(f"{BASE_URL}/api/payroll/employees/{emp_id}", timeout=20)
            pytest.skip("Run creation collided — skip mask test")
        run_id = r_run.json()["id"]

        me_check = admin_session.get(f"{BASE_URL}/api/me/permissions", timeout=20).json()
        slips = admin_session.get(f"{BASE_URL}/api/payroll/runs/{run_id}/payslips", timeout=20).json()
        match = next((s for s in slips if s["employee_id"] == emp_id), None)
        assert match
        if not me_check.get("is_super_admin"):
            # Super-admin bypasses masking; only assert for non-super-admin
            for k, v in match["computation"].items():
                # Only numeric keys should be masked; non-numeric (lop_days uses _r → number; all numeric) → masked
                assert v == "***", f"Expected '***' for masked field '{k}', got {v!r}"

        # Cleanup
        admin_session.delete(f"{BASE_URL}/api/payroll/runs/{run_id}", timeout=20)
        admin_session.delete(f"{BASE_URL}/api/payroll/employees/{emp_id}", timeout=20)
        admin_session.put(
            f"{BASE_URL}/api/admin/access/user/{admin_user_id}/modules",
            json={"modules": {"payroll": False}},
            timeout=20,
        )
