"""End-to-end backend tests for 5to sueño FastAPI + Socket.IO app."""
import os
import time
import pytest
import requests

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


# ---------------- Health ----------------
class TestHealth:
    def test_root(self, anon):
        r = anon.get(f"{BASE_URL}/api/")
        assert r.status_code == 200
        assert r.json().get("status") == "ok"

    def test_health(self, anon):
        r = anon.get(f"{BASE_URL}/api/health")
        assert r.status_code == 200
        assert r.json()["status"] == "healthy"


# ---------------- Auth ----------------
class TestAuth:
    def test_login_sets_cookies(self, anon):
        r = anon.post(
            f"{BASE_URL}/api/auth/login",
            json={"email": "admin@5tosueno.com", "password": "Admin5to2026!"},
        )
        assert r.status_code == 200, r.text
        data = r.json()
        assert data["email"] == "admin@5tosueno.com"
        assert data["role"] == "admin"
        # httpOnly cookies
        cookie_names = {c.name for c in r.cookies}
        assert "access_token" in cookie_names
        assert "refresh_token" in cookie_names

    def test_login_invalid(self, anon):
        r = anon.post(
            f"{BASE_URL}/api/auth/login",
            json={"email": "admin@5tosueno.com", "password": "WRONG"},
        )
        assert r.status_code == 401

    def test_me_requires_auth(self, anon):
        r = requests.get(f"{BASE_URL}/api/auth/me")
        assert r.status_code == 401

    def test_me_after_login(self, admin_sess):
        r = admin_sess.get(f"{BASE_URL}/api/auth/me")
        assert r.status_code == 200
        assert r.json()["role"] == "admin"

    def test_refresh(self, admin_sess):
        r = admin_sess.post(f"{BASE_URL}/api/auth/refresh")
        assert r.status_code == 200
        assert r.json().get("ok") is True

    def test_logout_clears_cookies(self):
        s = requests.Session()
        s.post(
            f"{BASE_URL}/api/auth/login",
            json={"email": "pm@5tosueno.com", "password": "Pm5to2026!"},
        )
        r = s.post(f"{BASE_URL}/api/auth/logout")
        assert r.status_code == 200
        # subsequent /me should fail
        r2 = s.get(f"{BASE_URL}/api/auth/me")
        assert r2.status_code == 401


# ---------------- RBAC users ----------------
class TestUsers:
    def test_client_can_list_users(self, client_sess):
        # backend currently does NOT restrict GET /users - documents behavior
        r = client_sess.get(f"{BASE_URL}/api/users")
        # Either 200 (current behavior) or 403 (stricter). Record actual:
        assert r.status_code in (200, 403)

    def test_only_admin_can_create_user(self, pm_sess):
        r = pm_sess.post(
            f"{BASE_URL}/api/users",
            json={
                "email": "TEST_pm_create@example.com",
                "name": "x",
                "password": "Aa12345!",
                "role": "collaborator",
                "send_invite": False,
            },
        )
        assert r.status_code == 403

    def test_admin_can_create_and_delete_user(self, admin_sess):
        email = f"test_user_{int(time.time())}@example.com"
        r = admin_sess.post(
            f"{BASE_URL}/api/users",
            json={
                "email": email,
                "name": "Tester",
                "password": "Aa12345!",
                "role": "collaborator",
                "send_invite": False,
            },
        )
        assert r.status_code == 200, r.text
        uid = r.json()["id"]
        assert r.json()["email"] == email
        # GET listing should include it
        r2 = admin_sess.get(f"{BASE_URL}/api/users")
        assert any(u["id"] == uid for u in r2.json())
        # delete
        rd = admin_sess.delete(f"{BASE_URL}/api/users/{uid}")
        assert rd.status_code == 200


# ---------------- Contact ----------------
class TestContact:
    def test_contact_persists_and_sends(self, anon):
        r = anon.post(
            f"{BASE_URL}/api/contact",
            json={
                "name": "Test Lead",
                "email": "lead@example.com",
                "company": "Acme",
                "message": "Hola, quiero un proyecto nuevo de prueba.",
            },
        )
        assert r.status_code == 200, r.text
        data = r.json()
        assert data["ok"] is True
        # email_sent may be False unless recipient is the verified address
        assert "email_sent" in data

    def test_contact_validation(self, anon):
        r = anon.post(
            f"{BASE_URL}/api/contact",
            json={"name": "x", "email": "bad-email", "message": "short"},
        )
        assert r.status_code == 422


# ---------------- CMS ----------------
class TestCMS:
    def test_public_services(self, anon):
        r = anon.get(f"{BASE_URL}/api/cms/services")
        assert r.status_code == 200
        assert isinstance(r.json(), list)
        assert len(r.json()) >= 1

    def test_public_testimonials(self, anon):
        r = anon.get(f"{BASE_URL}/api/cms/testimonials")
        assert r.status_code == 200
        assert len(r.json()) >= 1

    def test_public_portfolio(self, anon):
        r = anon.get(f"{BASE_URL}/api/cms/portfolio")
        assert r.status_code == 200

    def test_public_site_copy(self, anon):
        r = anon.get(f"{BASE_URL}/api/cms/site")
        assert r.status_code == 200
        assert "hero_title" in r.json()

    def test_pm_cannot_create_service(self, pm_sess):
        r = pm_sess.post(
            f"{BASE_URL}/api/cms/services",
            json={"title": "TEST", "description": "x", "icon": "Sparkles", "order": 99},
        )
        assert r.status_code == 403

    def test_admin_can_crud_service(self, admin_sess):
        r = admin_sess.post(
            f"{BASE_URL}/api/cms/services",
            json={"title": "TEST_Service", "description": "Tmp", "icon": "Sparkles", "order": 99},
        )
        assert r.status_code == 200
        sid = r.json()["id"]
        rd = admin_sess.delete(f"{BASE_URL}/api/cms/services/{sid}")
        assert rd.status_code == 200


# ---------------- Projects ----------------
@pytest.fixture(scope="module")
def created_project(admin_sess, user_ids):
    payload = {
        "name": f"TEST_Project_{int(time.time())}",
        "description": "test",
        "client_id": user_ids.get("cliente@empresa.com"),
        "pm_id": user_ids.get("pm@5tosueno.com"),
        "collaborator_ids": [user_ids.get("dev@5tosueno.com")],
        "status": "discovery",
    }
    r = admin_sess.post(f"{BASE_URL}/api/projects", json=payload)
    assert r.status_code == 200, r.text
    proj = r.json()
    yield proj
    admin_sess.delete(f"{BASE_URL}/api/projects/{proj['id']}")


class TestProjects:
    def test_create_project(self, created_project):
        assert created_project["id"]
        assert len(created_project["milestones"]) == 5

    def test_admin_list_sees_all(self, admin_sess, created_project):
        r = admin_sess.get(f"{BASE_URL}/api/projects")
        assert r.status_code == 200
        assert any(p["id"] == created_project["id"] for p in r.json())

    def test_client_sees_only_own(self, client_sess, created_project):
        r = client_sess.get(f"{BASE_URL}/api/projects")
        assert r.status_code == 200
        ids = [p["id"] for p in r.json()]
        # the test project includes them as client
        assert created_project["id"] in ids

    def test_collab_sees_assigned(self, collab_sess, created_project):
        r = collab_sess.get(f"{BASE_URL}/api/projects")
        assert r.status_code == 200
        assert any(p["id"] == created_project["id"] for p in r.json())

    def test_collab_cannot_create_project(self, collab_sess):
        r = collab_sess.post(f"{BASE_URL}/api/projects", json={"name": "x"})
        assert r.status_code == 403

    def test_milestones_update(self, admin_sess, created_project):
        new_mil = [
            {"label": "Descubrimiento", "completed": True},
            {"label": "Diseño aprobado", "completed": True},
            {"label": "En desarrollo", "completed": False},
        ]
        r = admin_sess.put(
            f"{BASE_URL}/api/projects/{created_project['id']}/milestones",
            json=new_mil,
        )
        assert r.status_code == 200, r.text
        # Verify persisted
        rg = admin_sess.get(f"{BASE_URL}/api/projects/{created_project['id']}")
        assert rg.status_code == 200
        assert len(rg.json()["milestones"]) == 3
        assert rg.json()["milestones"][1]["completed"] is True


# ---------------- Tasks ----------------
class TestTasks:
    def test_create_query_update_delete_task(self, admin_sess, created_project):
        pid = created_project["id"]
        r = admin_sess.post(
            f"{BASE_URL}/api/tasks",
            json={"project_id": pid, "title": "TEST_Task", "status": "backlog"},
        )
        assert r.status_code == 200
        tid = r.json()["id"]
        # list by project
        rl = admin_sess.get(f"{BASE_URL}/api/tasks", params={"project_id": pid})
        assert rl.status_code == 200
        assert any(t["id"] == tid for t in rl.json())
        # update status
        ru = admin_sess.patch(f"{BASE_URL}/api/tasks/{tid}", json={"status": "in_progress"})
        assert ru.status_code == 200
        assert ru.json()["status"] == "in_progress"
        # delete
        rd = admin_sess.delete(f"{BASE_URL}/api/tasks/{tid}")
        assert rd.status_code == 200

    def test_client_cannot_list_tasks(self, client_sess, created_project):
        r = client_sess.get(
            f"{BASE_URL}/api/tasks", params={"project_id": created_project["id"]}
        )
        assert r.status_code == 403


# ---------------- Chat (REST) ----------------
class TestChat:
    def test_admin_list_channels(self, admin_sess, created_project):
        r = admin_sess.get(f"{BASE_URL}/api/chat/channels")
        assert r.status_code == 200
        # at least the channel auto-created with project should exist
        names = [c["name"] for c in r.json()]
        assert any(created_project["name"] in n for n in names)

    def test_dm_open_or_reuse(self, admin_sess, user_ids):
        other = user_ids.get("pm@5tosueno.com")
        r1 = admin_sess.post(f"{BASE_URL}/api/chat/channels/dm/{other}")
        assert r1.status_code == 200
        r2 = admin_sess.post(f"{BASE_URL}/api/chat/channels/dm/{other}")
        assert r2.status_code == 200
        assert r1.json()["id"] == r2.json()["id"]

    def test_post_and_list_messages(self, admin_sess, created_project):
        # find the channel for our test project
        chans = admin_sess.get(f"{BASE_URL}/api/chat/channels").json()
        ch = next(c for c in chans if c.get("project_id") == created_project["id"])
        r = admin_sess.post(
            f"{BASE_URL}/api/chat/messages",
            json={"channel_id": ch["id"], "body": "Hola desde test"},
        )
        assert r.status_code == 200, r.text
        rl = admin_sess.get(f"{BASE_URL}/api/chat/channels/{ch['id']}/messages")
        assert rl.status_code == 200
        assert any(m["body"] == "Hola desde test" for m in rl.json())


# ---------------- Socket.IO realtime ----------------
class TestSocketIO:
    def test_socketio_realtime_message(self, admin_sess, created_project):
        import socketio as sio_client

        chans = admin_sess.get(f"{BASE_URL}/api/chat/channels").json()
        ch = next(c for c in chans if c.get("project_id") == created_project["id"])
        # extract access token cookie
        token = admin_sess.cookies.get("access_token")
        assert token

        client = sio_client.Client(reconnection=False)
        received = []

        @client.on("message")
        def _on_msg(data):
            received.append(data)

        client.connect(
            BASE_URL,
            socketio_path="/api/socket.io",
            auth={"token": token},
            transports=["websocket"],
            wait_timeout=10,
        )
        ack = client.call("join_channel", {"channel_id": ch["id"]}, timeout=5)
        assert ack and ack.get("ok") is True

        admin_sess.post(
            f"{BASE_URL}/api/chat/messages",
            json={"channel_id": ch["id"], "body": "RT_TEST"},
        )
        # wait for broadcast
        for _ in range(20):
            if any(m.get("body") == "RT_TEST" for m in received):
                break
            time.sleep(0.25)
        client.disconnect()
        assert any(m.get("body") == "RT_TEST" for m in received), "Realtime broadcast not received"
