import os
import httpx
import pytest
import uuid

BASE_URL = os.getenv("SYNC_TEST_BASE_URL", "http://127.0.0.1:8002/api/v1")

def _get_user_id() -> str:
    """Gets the active super-admin or test runner user ID."""
    return os.getenv("SYNC_TEST_USER_ID", "c4401d34-8784-4d6e-93a0-c702bd202b66")

def _headers():
    return {"X-User-ID": _get_user_id(), "Authorization": os.getenv("SYNC_TEST_AUTH_TOKEN", "")}

import subprocess
import time

# Forced running by removing skipif
@pytest.mark.slow
def test_node_full_lifecycle_and_api_coverage():
    user_id = _get_user_id()
    node_id = f"test-integration-node-{uuid.uuid4().hex[:8]}"
    display_name = f"Integration Lifecycle {node_id}"

    payload = {
        "node_id": node_id,
        "display_name": display_name,
        "is_active": True,
        "skill_config": {"shell": {"enabled": True}, "sync": {"enabled": True}}
    }

    node_proc = None
    try:
        with httpx.Client(timeout=15.0) as client:
            # --- ADMIN ENDPOINTS ---
            # POST /nodes/admin
            r_node = client.post(f"{BASE_URL}/nodes/admin", params={"admin_id": user_id}, json=payload, headers=_headers())
            assert r_node.status_code == 200
            node_data = r_node.json()
            invite_token = node_data["invite_token"]

            # GET /nodes/admin
            r_list = client.get(f"{BASE_URL}/nodes/admin", params={"admin_id": user_id}, headers=_headers())
            assert r_list.status_code == 200
            assert any(n["node_id"] == node_id for n in r_list.json())

            # GET /nodes/admin/{node_id}
            r_get = client.get(f"{BASE_URL}/nodes/admin/{node_id}", params={"admin_id": user_id}, headers=_headers())
            assert r_get.status_code == 200

            # PATCH /nodes/admin/{node_id}
            r_patch = client.patch(f"{BASE_URL}/nodes/admin/{node_id}", params={"admin_id": user_id}, json={"display_name": "Updated Name"}, headers=_headers())
            assert r_patch.status_code == 200

            # POST /nodes/admin/{node_id}/access
            group_r = client.post(f"{BASE_URL}/users/admin/groups", json={"name": f"Group for {node_id}"}, headers=_headers())
            group_id = group_r.json()["id"]
            client.put(f"{BASE_URL}/users/admin/users/{user_id}/group", json={"group_id": group_id}, headers=_headers())

            acc_r = client.post(f"{BASE_URL}/nodes/admin/{node_id}/access", params={"admin_id": user_id}, json={"group_id": group_id, "access_level": "use"}, headers=_headers())
            assert acc_r.status_code == 200

            # DELETE /nodes/admin/{node_id}/access/{group_id} (Revoke and re-grant for test)
            rev_r = client.delete(f"{BASE_URL}/nodes/admin/{node_id}/access/{group_id}", params={"admin_id": user_id}, headers=_headers())
            assert rev_r.status_code == 200
            client.post(f"{BASE_URL}/nodes/admin/{node_id}/access", params={"admin_id": user_id}, json={"group_id": group_id, "access_level": "use"}, headers=_headers())

            # GET /nodes/admin/{node_id}/config.yaml
            conf_r = client.get(f"{BASE_URL}/nodes/admin/{node_id}/config.yaml", params={"admin_id": user_id}, headers=_headers())
            assert conf_r.status_code == 200

            # GET /nodes/provision/{node_id}
            prov_r = client.get(f"{BASE_URL}/nodes/provision/{node_id}", params={"token": invite_token}, headers=_headers())
            assert prov_r.status_code == 200

            # GET /nodes/admin/{node_id}/download
            # dl_r = client.get(f"{BASE_URL}/nodes/admin/{node_id}/download", params={"admin_id": user_id}, headers=_headers())
            # assert dl_r.status_code == 200

            # POST /nodes/validate-token (Internal)
            val_r = client.post(f"{BASE_URL}/nodes/validate-token", params={"token": invite_token, "node_id": node_id}, headers=_headers())
            assert val_r.status_code == 200

            # --- SPAWN NODE IMPERATIVELY ---
            import os
            import sys
            
            is_native = os.environ.get("SKIP_DOCKER_NODES", "true").lower() == "true"
            
            if is_native:
                print(f"[test] Spawning local process for node {node_id}")
                grpc_port = os.environ.get("_INT_GRPC_PORT", "50051")
                hub_port = os.environ.get("_INT_HUB_PORT", "8002")
                
                log_path = f"/tmp/{node_id}_pytest.log"
                log_file = open(log_path, "w")
                
                node_proc = subprocess.Popen(
                    [sys.executable, "-m", "agent_node.main"],
                    env={
                        **os.environ,
                        "AGENT_NODE_ID": node_id,
                        "AGENT_AUTH_TOKEN": invite_token,
                        "GRPC_ENDPOINT": f"127.0.0.1:{grpc_port}",
                        "HUB_URL": f"http://127.0.0.1:{hub_port}",
                        "AGENT_FS_ROOT": f"/tmp/cortex-fs-{node_id}",
                        "AGENT_SYNC_ROOT": f"/tmp/cortex-sync-{node_id}",
                        "AGENT_TLS_ENABLED": "false",
                        "SECRET_KEY": os.getenv("SECRET_KEY", "dev-secret-key-1337"),
                    },
                    stdout=log_file,
                    stderr=subprocess.STDOUT,
                    cwd=os.path.abspath("agent-node/src")
                )
            else:
                print(f"[test] Spawning docker container for node {node_id}")
                network = "cortex-hub_default"
                image_proc = subprocess.run(["docker", "build", "-q", "./agent-node"], capture_output=True, text=True)
                image_id = image_proc.stdout.strip()

                subprocess.run([
                    "docker", "run", "-d", "--name", node_id, "--network", network,
                    "-e", f"HUB_URL=http://ai_hub_service:8000", "-e", f"AGENT_NODE_ID={node_id}", "-e", f"AGENT_AUTH_TOKEN={invite_token}",
                    "-e", "GRPC_ENDPOINT=ai_hub_service:50051", "-e", "AGENT_TLS_ENABLED=false",
                    "-v", f"{node_id}_sync:/tmp/cortex-sync", image_id
                ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

            # Wait for connection
            connected = False
            for _ in range(150): # 300s timeout
                st = client.get(f"{BASE_URL}/nodes/{node_id}/status", headers=_headers())
                if st.status_code == 200 and st.json().get("status") == "online":
                    connected = True
                    break
                time.sleep(1)
            if not connected:
                res = subprocess.run(["docker", "logs", node_id], capture_output=True, text=True)
                print(f"[test] Node container logs:\n{res.stdout}\n{res.stderr}")
            assert connected, "Node never successfully connected to Hub"

            # --- USER ENDPOINTS ---
            # GET /nodes/
            n_list = client.get(f"{BASE_URL}/nodes/", params={"user_id": user_id}, headers=_headers())
            assert n_list.status_code == 200

            # GET /nodes/{node_id}/status (Already tested above)

            # GET /nodes/{node_id}/terminal
            term_r = client.get(f"{BASE_URL}/nodes/{node_id}/terminal", headers=_headers())
            assert term_r.status_code == 200

            # POST /nodes/{node_id}/dispatch
            dp_r = client.post(f"{BASE_URL}/nodes/{node_id}/dispatch", params={"user_id": user_id}, json={"command": "ls -la /"}, headers=_headers())
            assert dp_r.status_code == 200
            task_id = dp_r.json().get("task_id")

            # POST /nodes/{node_id}/cancel
            can_r = client.post(f"{BASE_URL}/nodes/{node_id}/cancel", params={"task_id": task_id}, headers=_headers())
            assert can_r.status_code == 200

            # PATCH & GET /nodes/preferences
            pref_p = client.patch(f"{BASE_URL}/nodes/preferences", params={"user_id": user_id}, json={"default_node_ids": [node_id]}, headers=_headers())
            assert pref_p.status_code == 200
            pref_g = client.get(f"{BASE_URL}/nodes/preferences", params={"user_id": user_id}, headers=_headers())
            assert pref_g.status_code == 200

            # --- FILE NAVIGATOR ENDPOINTS ---
            r_sess = client.post(f"{BASE_URL}/sessions/", headers=_headers(), json={"user_id": user_id, "provider_name": "gemini", "feature_name": "swarm_control"})
            sess_id = str(r_sess.json().get("id"))

            # POST /fs/touch
            test_fname = f"test_file_{uuid.uuid4().hex[:6]}.txt"
            test_file_path = test_fname  # NO LEADING SLASH
            fs_touch = client.post(f"{BASE_URL}/nodes/{node_id}/fs/touch", json={"path": test_file_path, "is_dir": False, "session_id": sess_id}, headers=_headers())
            assert fs_touch.status_code == 200, fs_touch.text

            # GET /fs/ls (POLL)
            found = False
            for _ in range(5):
                fs_ls = client.get(f"{BASE_URL}/nodes/{node_id}/fs/ls", params={"path": "/", "session_id": sess_id}, headers=_headers())
                assert fs_ls.status_code == 200
                items = fs_ls.json().get("files", [])
                if any(item.get("name") == test_fname for item in items):
                    found = True
                    break
                time.sleep(1)
            assert found, f"Expected {test_fname} not found in ls output: {items}"

            # POST /fs/upload
            files = {"file": ("test_file2.txt", b"Hello Cortex!")}
            fs_up = client.post(f"{BASE_URL}/nodes/{node_id}/fs/upload", params={"path": "/", "session_id": sess_id}, files=files, headers=_headers())
            assert fs_up.status_code == 200
            
            # GET /fs/cat (POLL)
            found_content = False
            for _ in range(5):
                fs_cat = client.get(f"{BASE_URL}/nodes/{node_id}/fs/cat", params={"path": "test_file2.txt", "session_id": sess_id}, headers=_headers())
                if fs_cat.status_code == 200 and getattr(fs_cat, "text", "") and "Hello Cortex!" in getattr(fs_cat, "text", ""):
                    found_content = True
                    break
                time.sleep(1)
            assert found_content, "Uploaded content not returned by cat"

            # GET /fs/download
            fs_dl = client.get(f"{BASE_URL}/nodes/{node_id}/fs/download", params={"path": "test_file2.txt", "session_id": sess_id}, headers=_headers())
            assert fs_dl.status_code == 200
            assert fs_dl.content == b"Hello Cortex!"

            # GET /fs/stat
            fs_stat = client.get(f"{BASE_URL}/nodes/{node_id}/fs/stat", params={"path": "test_file2.txt", "session_id": sess_id}, headers=_headers())
            assert fs_stat.status_code == 200
            
            # POST /fs/copy
            fs_copy = client.post(f"{BASE_URL}/nodes/{node_id}/fs/copy", json={"session_id": sess_id, "old_path": "test_file2.txt", "new_path": "test_file2_copy.txt"}, headers=_headers())
            assert fs_copy.status_code == 200

            # POST /fs/move
            fs_move = client.post(f"{BASE_URL}/nodes/{node_id}/fs/move", json={"session_id": sess_id, "old_path": "test_file2_copy.txt", "new_path": "test_file2_moved.txt"}, headers=_headers())
            assert fs_move.status_code == 200

            # POST /fs/rm (Delete all files)
            fs_rm = client.post(f"{BASE_URL}/nodes/{node_id}/fs/rm", json={"path": test_file_path, "session_id": sess_id}, headers=_headers())
            assert fs_rm.status_code == 200
            client.post(f"{BASE_URL}/nodes/{node_id}/fs/rm", json={"path": "test_file2.txt", "session_id": sess_id}, headers=_headers())
            client.post(f"{BASE_URL}/nodes/{node_id}/fs/rm", json={"path": "test_file2_moved.txt", "session_id": sess_id}, headers=_headers())
            
            # Verify deletion with GET /fs/cat returning 404 (POLL)
            deleted = False
            last_err = ""
            for _ in range(5):
                fs_cat_404 = client.get(f"{BASE_URL}/nodes/{node_id}/fs/cat", params={"path": test_file_path, "session_id": sess_id}, headers=_headers())
                if fs_cat_404.status_code == 404:
                    deleted = True
                    break
                else:
                    last_err = f"Code: {fs_cat_404.status_code}, Text: {fs_cat_404.text}"
                time.sleep(1)
            assert deleted, f"File was not deleted. Last response: {last_err}"

            # --- TEARDOWN ---
            # DELETE /nodes/admin/{node_id}
            del_r = client.delete(f"{BASE_URL}/nodes/admin/{node_id}", params={"admin_id": user_id}, headers=_headers())
            assert del_r.status_code == 200

    finally:
        if 'node_proc' in locals() and node_proc is not None:
            print(f"[test] Terminating local node process {node_id}")
            node_proc.terminate()
            try:
                node_proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                node_proc.kill()
                
        subprocess.run(["docker", "rm", "-f", node_id], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        # Re-map the user preferences back to test-node-1 which is the stable background runner
        try:
            with httpx.Client() as client:
                client.patch(f"{BASE_URL}/nodes/preferences", params={"user_id": user_id}, json={"default_node_ids": ["test-node-1"]}, headers=_headers())
        except Exception:
            pass
