"""Iteration 2 — tests for new modules: activation, departments, CRM,
marketing, notifications, search, proposals, expanded RBAC + chat."""
import os
import time
import requests
import pytest

BASE_URL = os.environ.get(
    "REACT_APP_BACKEND_URL", "https://quinto-sueno.preview.emergentagent.com"
).rstrip("/")


# ---------------- RBAC on user-management ----------------
class TestUserMgmtRBAC:
    def _payload(self, suffix=""):
        return {
            "email": f"TEST_um_{int(time.time()*1000)}{suffix}@example.com",
            "name": "Tester",
            "role": "collaborator",
            "send_invite": False,
        }

    def test_pm_cannot_create_user(self, pm_sess):
        r = pm_sess.post(f"{BASE_URL}/api/users", json=self._payload("a"))
        assert r.status_code == 403

    def test_sales_cannot_create_user(self, sales_sess):
        r = sales_sess.post(f"{BASE_URL}/api/users", json=self._payload("b"))
        assert r.status_code == 403

    def test_marketing_cannot_create_user(self, marketing_sess):
        r = marketing_sess.post(f"{BASE_URL}/api/users", json=self._payload("c"))
        assert r.status_code == 403

    def test_ceo_can_create_user(self, ceo_sess):
        p = self._payload("d")
        r = ceo_sess.post(f"{BASE_URL}/api/users", json=p)
        assert r.status_code == 200, r.text
        ceo_sess.delete(f"{BASE_URL}/api/users/{r.json()['id']}")

    def test_rh_can_create_user(self, rh_sess):
        p = self._payload("e")
        r = rh_sess.post(f"{BASE_URL}/api/users", json=p)
        assert r.status_code == 200, r.text
        rh_sess.delete(f"{BASE_URL}/api/users/{r.json()['id']}")

    def test_client_cannot_list_users(self, client_sess):
        # iteration_1 noted this was open; should now be 403
        r = client_sess.get(f"{BASE_URL}/api/users")
        assert r.status_code == 403


# ---------------- Activation flow ----------------
@pytest.fixture
def activation_user(admin_sess):
    email = f"TEST_act_{int(time.time()*1000)}@example.com"
    r = admin_sess.post(
        f"{BASE_URL}/api/users",
        json={"email": email, "name": "Act Tester", "role": "collaborator", "send_invite": False},
    )
    assert r.status_code == 200, r.text
    data = r.json()
    assert "_activation_link" in data
    assert "_temp_password" in data
    assert data["active"] is False
    assert data["must_change_password"] is True
    yield data
    admin_sess.delete(f"{BASE_URL}/api/users/{data['id']}")


class TestActivation:
    def test_create_user_returns_activation_link(self, activation_user):
        link = activation_user["_activation_link"]
        assert "/activate/" in link
        # extract token
        token = link.rsplit("/", 1)[-1]
        assert len(token) > 20

    def test_inactive_user_cannot_login(self, activation_user):
        r = requests.post(
            f"{BASE_URL}/api/auth/login",
            json={"email": activation_user["email"], "password": activation_user["_temp_password"]},
        )
        assert r.status_code == 403, r.text

    def test_activation_get_valid(self, activation_user):
        token = activation_user["_activation_link"].rsplit("/", 1)[-1]
        r = requests.get(f"{BASE_URL}/api/auth/activation/{token}")
        assert r.status_code == 200, r.text
        assert r.json()["valid"] is True
        assert r.json()["email"] == activation_user["email"]

    def test_activation_get_invalid(self):
        r = requests.get(f"{BASE_URL}/api/auth/activation/not-a-real-token-xyz")
        assert r.status_code == 404

    def test_activate_and_autologin(self, activation_user):
        token = activation_user["_activation_link"].rsplit("/", 1)[-1]
        s = requests.Session()
        r = s.post(
            f"{BASE_URL}/api/auth/activate",
            json={"token": token, "new_password": "BrandNewPass1!"},
        )
        assert r.status_code == 200, r.text
        assert r.json()["email"] == activation_user["email"]
        # auth cookies set
        names = {c.name for c in s.cookies}
        assert "access_token" in names and "refresh_token" in names
        # /me works
        rm = s.get(f"{BASE_URL}/api/auth/me")
        assert rm.status_code == 200
        # Now can login normally
        r2 = requests.post(
            f"{BASE_URL}/api/auth/login",
            json={"email": activation_user["email"], "password": "BrandNewPass1!"},
        )
        assert r2.status_code == 200

    def test_resend_activation(self, admin_sess, activation_user):
        r = admin_sess.post(f"{BASE_URL}/api/users/{activation_user['id']}/resend-activation")
        assert r.status_code == 200, r.text
        assert "activation_link" in r.json()
        # new token validates
        new_token = r.json()["activation_link"].rsplit("/", 1)[-1]
        g = requests.get(f"{BASE_URL}/api/auth/activation/{new_token}")
        assert g.status_code == 200


# ---------------- Departments ----------------
class TestDepartments:
    def test_list_any_auth(self, pm_sess):
        r = pm_sess.get(f"{BASE_URL}/api/departments")
        assert r.status_code == 200
        assert isinstance(r.json(), list)

    def test_pm_cannot_create(self, pm_sess):
        r = pm_sess.post(f"{BASE_URL}/api/departments", json={"name": "TEST_pm_dept"})
        assert r.status_code == 403

    def test_admin_create_creates_channel(self, admin_sess):
        name = f"TEST_Dept_{int(time.time()*1000)}"
        r = admin_sess.post(f"{BASE_URL}/api/departments", json={"name": name, "description": "x"})
        assert r.status_code == 200, r.text
        dept = r.json()
        # auto-channel
        chans = admin_sess.get(f"{BASE_URL}/api/chat/channels").json()
        ch = next((c for c in chans if c.get("department_id") == dept["id"]), None)
        assert ch is not None, "Department channel not created"
        assert ch["type"] == "department"
        assert ch["name"].startswith("Depto:")
        # cleanup
        admin_sess.delete(f"{BASE_URL}/api/departments/{dept['id']}")

    def test_update_and_delete(self, admin_sess):
        r = admin_sess.post(f"{BASE_URL}/api/departments", json={"name": f"TEST_DeptU_{int(time.time()*1000)}"})
        d = r.json()
        ru = admin_sess.patch(f"{BASE_URL}/api/departments/{d['id']}", json={"name": d["name"], "description": "edited"})
        assert ru.status_code == 200
        assert ru.json()["description"] == "edited"
        rd = admin_sess.delete(f"{BASE_URL}/api/departments/{d['id']}")
        assert rd.status_code == 200


# ---------------- CRM ----------------
@pytest.fixture
def crm_client(sales_sess):
    payload = {
        "name": f"TEST_CRM_{int(time.time()*1000)}",
        "email": "crmtest@example.com",
        "company": "Acme",
        "status": "lead",
        "tags": ["spring", "test"],
    }
    r = sales_sess.post(f"{BASE_URL}/api/crm/clients", json=payload)
    assert r.status_code == 200, r.text
    c = r.json()
    yield c
    # cleanup via admin
    requests.post(f"{BASE_URL}/api/auth/login", json={"email": "admin@5tosueno.com", "password": "Admin5to2026!"})


class TestCRM:
    def test_collaborator_forbidden(self, collab_sess):
        r = collab_sess.get(f"{BASE_URL}/api/crm/clients")
        assert r.status_code == 403

    def test_client_forbidden(self, client_sess):
        r = client_sess.get(f"{BASE_URL}/api/crm/clients")
        assert r.status_code == 403

    def test_rh_forbidden(self, rh_sess):
        r = rh_sess.get(f"{BASE_URL}/api/crm/clients")
        assert r.status_code == 403

    def test_sales_can_create_list(self, sales_sess, crm_client):
        r = sales_sess.get(f"{BASE_URL}/api/crm/clients")
        assert r.status_code == 200
        assert any(c["id"] == crm_client["id"] for c in r.json())

    def test_marketing_can_list(self, marketing_sess, crm_client):
        r = marketing_sess.get(f"{BASE_URL}/api/crm/clients", params={"status": "lead"})
        assert r.status_code == 200
        assert all(c["status"] == "lead" for c in r.json())

    def test_account_manager_can_list(self, account_sess):
        r = account_sess.get(f"{BASE_URL}/api/crm/clients")
        assert r.status_code == 200

    def test_search_q(self, sales_sess, crm_client):
        r = sales_sess.get(f"{BASE_URL}/api/crm/clients", params={"q": "TEST_CRM"})
        assert r.status_code == 200
        assert any(c["id"] == crm_client["id"] for c in r.json())

    def test_add_note_bumps_last_contact(self, sales_sess, crm_client):
        r = sales_sess.post(
            f"{BASE_URL}/api/crm/clients/{crm_client['id']}/notes",
            json={"body": "TEST note body"},
        )
        assert r.status_code == 200
        # verify last_contact_at set
        g = sales_sess.get(f"{BASE_URL}/api/crm/clients/{crm_client['id']}")
        assert g.status_code == 200
        assert g.json().get("last_contact_at") is not None
        assert any(n["body"] == "TEST note body" for n in g.json()["activity_notes"])

    def test_update_client_status(self, sales_sess, crm_client):
        r = sales_sess.patch(
            f"{BASE_URL}/api/crm/clients/{crm_client['id']}",
            json={"status": "active"},
        )
        assert r.status_code == 200
        assert r.json()["status"] == "active"


# ---------------- Marketing ----------------
class TestMarketing:
    def test_collab_cannot_access(self, collab_sess):
        r = collab_sess.get(f"{BASE_URL}/api/marketing/templates")
        assert r.status_code == 403

    def test_template_crud(self, marketing_sess):
        r = marketing_sess.post(
            f"{BASE_URL}/api/marketing/templates",
            json={"name": "TEST_tpl", "subject": "Hola {{name}}", "body_html": "<p>Hi {{name}}</p>"},
        )
        assert r.status_code == 200
        tid = r.json()["id"]
        rl = marketing_sess.get(f"{BASE_URL}/api/marketing/templates")
        assert any(t["id"] == tid for t in rl.json())
        ru = marketing_sess.patch(
            f"{BASE_URL}/api/marketing/templates/{tid}",
            json={"name": "TEST_tpl2", "subject": "S", "body_html": "B"},
        )
        assert ru.status_code == 200
        assert ru.json()["name"] == "TEST_tpl2"
        rd = marketing_sess.delete(f"{BASE_URL}/api/marketing/templates/{tid}")
        assert rd.status_code == 200

    def test_send_single_with_variable_substitution(self, marketing_sess, crm_client):
        r = marketing_sess.post(
            f"{BASE_URL}/api/marketing/send",
            json={
                "to": "marketing-target@example.com",
                "subject": "Hola {{name}}",
                "body_html": "<p>{{name}} de {{company}}</p>",
                "crm_client_id": crm_client["id"],
            },
        )
        # may succeed or fail email_sent — but request is accepted
        assert r.status_code == 200, r.text
        # log entry exists
        lg = marketing_sess.get(f"{BASE_URL}/api/marketing/log").json()
        assert any(l["to"] == "marketing-target@example.com" for l in lg)

    def test_campaign_filters_by_audience_status(self, marketing_sess, crm_client):
        # crm_client default status=lead → it is in audience
        r = marketing_sess.post(
            f"{BASE_URL}/api/marketing/campaigns",
            json={
                "name": f"TEST_camp_{int(time.time())}",
                "subject": "S {{name}}",
                "body_html": "<p>Hola {{name}}</p>",
                "audience_status": "lead",
            },
        )
        assert r.status_code == 200, r.text
        data = r.json()
        assert data["audience_count"] >= 1
        # campaign listed
        rl = marketing_sess.get(f"{BASE_URL}/api/marketing/campaigns").json()
        assert any(c["id"] == data["id"] for c in rl)

    def test_campaign_empty_audience(self, marketing_sess):
        r = marketing_sess.post(
            f"{BASE_URL}/api/marketing/campaigns",
            json={"name": "TEST_empty", "subject": "x", "body_html": "y", "audience_status": "won", "audience_emails": []},
        )
        # may have 0 audience → 400
        assert r.status_code in (200, 400)


# ---------------- Notifications ----------------
class TestNotifications:
    def test_list_unread_count(self, admin_sess):
        r = admin_sess.get(f"{BASE_URL}/api/notifications")
        assert r.status_code == 200
        r2 = admin_sess.get(f"{BASE_URL}/api/notifications/unread-count")
        assert r2.status_code == 200
        assert "count" in r2.json()

    def test_read_all(self, admin_sess):
        r = admin_sess.post(f"{BASE_URL}/api/notifications/read-all")
        assert r.status_code == 200
        c = admin_sess.get(f"{BASE_URL}/api/notifications/unread-count").json()
        assert c["count"] == 0

    def test_proposal_triggers_notification(self, client_sess, admin_sess):
        # mark all read first as baseline
        admin_sess.post(f"{BASE_URL}/api/notifications/read-all")
        r = client_sess.post(
            f"{BASE_URL}/api/proposals",
            json={"title": f"TEST_prop_{int(time.time())}", "description": "Need a new web project please"},
        )
        assert r.status_code == 200, r.text
        pid = r.json()["id"]
        time.sleep(0.5)
        cnt = admin_sess.get(f"{BASE_URL}/api/notifications/unread-count").json()["count"]
        assert cnt >= 1, "admin should receive proposal notification"
        # cleanup
        admin_sess.delete(f"{BASE_URL}/api/proposals/{pid}")


# ---------------- Search ----------------
class TestSearch:
    def test_admin_search_all_buckets(self, admin_sess):
        r = admin_sess.get(f"{BASE_URL}/api/search", params={"q": "5to"})
        assert r.status_code == 200
        data = r.json()
        for k in ("projects", "tasks", "users", "clients", "departments"):
            assert k in data

    def test_client_search_no_users_no_crm(self, client_sess):
        r = client_sess.get(f"{BASE_URL}/api/search", params={"q": "admin"})
        assert r.status_code == 200
        d = r.json()
        assert d["users"] == []
        assert d["clients"] == []

    def test_collab_search_no_crm(self, collab_sess):
        r = collab_sess.get(f"{BASE_URL}/api/search", params={"q": "a"})
        assert r.status_code == 200
        assert r.json()["clients"] == []


# ---------------- Proposals ----------------
class TestProposals:
    def test_collaborator_cannot_submit(self, collab_sess):
        r = collab_sess.post(
            f"{BASE_URL}/api/proposals",
            json={"title": "TEST_x", "description": "should be forbidden, only clients"},
        )
        assert r.status_code == 403

    def test_client_submit_and_list_own(self, client_sess, admin_sess):
        r = client_sess.post(
            f"{BASE_URL}/api/proposals",
            json={"title": f"TEST_prop2_{int(time.time())}", "description": "another test proposal description"},
        )
        assert r.status_code == 200
        pid = r.json()["id"]
        rl = client_sess.get(f"{BASE_URL}/api/proposals")
        assert any(p["id"] == pid for p in rl.json())
        admin_sess.delete(f"{BASE_URL}/api/proposals/{pid}")

    def test_viewers_see_all_clients_see_own(self, client_sess, admin_sess, sales_sess):
        r = client_sess.post(
            f"{BASE_URL}/api/proposals",
            json={"title": f"TEST_prop3_{int(time.time())}", "description": "proposal description text"},
        )
        pid = r.json()["id"]
        # admin viewer sees it
        ra = admin_sess.get(f"{BASE_URL}/api/proposals").json()
        assert any(p["id"] == pid for p in ra)
        # sales viewer sees it
        rs = sales_sess.get(f"{BASE_URL}/api/proposals").json()
        assert any(p["id"] == pid for p in rs)
        # admin can update
        ru = admin_sess.patch(f"{BASE_URL}/api/proposals/{pid}", json={"status": "reviewing"})
        assert ru.status_code == 200
        assert ru.json()["status"] == "reviewing"
        admin_sess.delete(f"{BASE_URL}/api/proposals/{pid}")

    def test_rh_cannot_view_proposals(self, rh_sess):
        r = rh_sess.get(f"{BASE_URL}/api/proposals")
        assert r.status_code == 403


# ---------------- Chat extensions ----------------
class TestChatExtensions:
    def test_create_team_channel_admin(self, admin_sess):
        r = admin_sess.post(
            f"{BASE_URL}/api/chat/channels",
            json={"name": f"TEST_team_{int(time.time())}", "type": "team", "member_ids": []},
        )
        assert r.status_code == 200
        assert r.json()["type"] == "team"

    def test_marketing_cannot_create_team_channel(self, marketing_sess):
        r = marketing_sess.post(
            f"{BASE_URL}/api/chat/channels",
            json={"name": "TEST_nope", "type": "team"},
        )
        assert r.status_code == 403

    def test_collab_cannot_see_client_channels_not_in(self, collab_sess, admin_sess, user_ids):
        # find / create a client channel (project channel of cliente is type=project, but
        # test using a synthetic restriction: collaborator should not see channels they
        # are not a member of). Just verify response is 200 + the filter holds.
        r = collab_sess.get(f"{BASE_URL}/api/chat/channels")
        assert r.status_code == 200
        # all returned channels must include collab uid in members
        collab_id = user_ids["dev@5tosueno.com"]
        for c in r.json():
            assert collab_id in (c.get("members") or []), f"collab sees channel they aren't in: {c}"
