from __future__ import annotations

from pathlib import Path

import yaml
from fastapi.testclient import TestClient


def _write_config(tmp_path: Path) -> Path:
    cfg = {
        "ray": {
            "address": "http://127.0.0.1:8265",
            "shared_root": "/private",
            "entrypoint_resources": {"worker_node": 1},
            "runtime_env": {"env_vars": {}},
        },
        "data": {
            "user_root": str(tmp_path / "users"),
        },
        "service": {
            "api": {"host": "127.0.0.1", "port": 0},
            "auth": {"token_env": "MVP_INTERNAL_TOKEN"},
            "sqlite": {"db_path": str(tmp_path / "mvp.sqlite3")},
            "scheduler": {"tick_s": 1, "retry_interval_s": 1, "max_running_tasks": 1},
        },
        "serving": {
            "serve": {"http_port": 8000, "proxy_location": "HeadOnly"},
            "llm": {"accelerator_type": "H20"},
        },
    }
    p = tmp_path / "cfg.yaml"
    p.write_text(yaml.safe_dump(cfg), encoding="utf-8")
    return p


def test_serving_api_crud_flow(tmp_path: Path, monkeypatch):
    from argus.service import app as app_mod

    cfg_path = _write_config(tmp_path)
    monkeypatch.setenv("MVP_INTERNAL_TOKEN", "admin-token")

    class _Scheduler:
        def __init__(self, **kwargs):
            self.tool = object()

        def run_forever(self, stop_flag):
            return None

    monkeypatch.setattr(app_mod, "Scheduler", _Scheduler)
    monkeypatch.setattr(app_mod, "new_model_key", lambda user_id: f"mk-{user_id}")

    app = app_mod.create_app(str(cfg_path))

    admin_headers = {"authorization": "Bearer admin-token"}
    with TestClient(app) as c:
        r = c.post("/api/v2/users", headers=admin_headers, json={"user_id": "alice"})
        assert r.status_code == 200
        r2 = c.post("/api/v2/users/alice/tokens", headers=admin_headers)
        assert r2.status_code == 200
        user_token = r2.json()["token"]

        headers = {"authorization": f"Bearer {user_token}"}

        spec_yaml = (
            "model_id: qwen-0.5b\n"
            "model_source: $HOME/common/hf/models--Qwen--Qwen2.5-0.5B-Instruct/snapshots/sha\n"
            "num_replicas: 1\n"
            "gpus_per_replica: 1\n"
        )
        r3 = c.post("/api/v2/serve/models", headers=headers, data=spec_yaml)
        assert r3.status_code == 200
        assert r3.json()["model_key"] == "mk-alice"
        assert r3.json()["state"] == "QUEUED"

        r4 = c.get("/api/v2/serve/models?limit=10&offset=0", headers=headers)
        assert r4.status_code == 200
        obj = r4.json()
        assert obj["openai_base_url"] is None
        assert len(obj["items"]) == 1
        assert obj["items"][0]["model_key"] == "mk-alice"
        assert obj["items"][0]["endpoint"]["openai_base_url"] == "http://testserver:8000/serve/mk-alice/v1"

        r5 = c.get("/api/v2/serve/models/mk-alice", headers=headers)
        assert r5.status_code == 200
        detail = r5.json()
        assert detail["model"]["model_key"] == "mk-alice"
        assert "model_id_prefix" in detail["model"]
        assert "resolved_spec_yaml" in detail
        assert isinstance(detail.get("events"), list)

        r6 = c.patch("/api/v2/serve/models/mk-alice", headers=headers, json={"num_replicas": 2})
        assert r6.status_code == 200
        assert r6.json()["state"] == "QUEUED"

        r7 = c.delete("/api/v2/serve/models/mk-alice", headers=headers)
        assert r7.status_code == 200
        assert r7.json()["state"] == "DELETING"

        # Admin status endpoint
        r8 = c.get("/api/v2/serve/status", headers=admin_headers)
        assert r8.status_code == 200
        assert r8.json()["http_port"] == 8000
        assert r8.json()["openai_base_url"] is None


def test_serving_api_rejects_path_outside_user_and_hf(tmp_path: Path, monkeypatch):
    from argus.service import app as app_mod

    cfg_path = _write_config(tmp_path)
    monkeypatch.setenv("MVP_INTERNAL_TOKEN", "admin-token")

    class _Scheduler:
        def __init__(self, **kwargs):
            self.tool = object()

        def run_forever(self, stop_flag):
            return None

    monkeypatch.setattr(app_mod, "Scheduler", _Scheduler)
    monkeypatch.setattr(app_mod, "new_model_key", lambda user_id: f"mk-{user_id}")

    app = app_mod.create_app(str(cfg_path))

    admin_headers = {"authorization": "Bearer admin-token"}
    with TestClient(app) as c:
        c.post("/api/v2/users", headers=admin_headers, json={"user_id": "alice"})
        r2 = c.post("/api/v2/users/alice/tokens", headers=admin_headers)
        user_token = r2.json()["token"]
        headers = {"authorization": f"Bearer {user_token}"}

        spec_yaml = (
            "model_id: x\n"
            "model_source: /private/users/bob/models/evil\n"
            "num_replicas: 1\n"
            "gpus_per_replica: 1\n"
        )
        r3 = c.post("/api/v2/serve/models", headers=headers, data=spec_yaml)
        assert r3.status_code == 403


def test_serving_api_invalid_yaml_and_non_mapping(tmp_path: Path, monkeypatch):
    from argus.service import app as app_mod

    cfg_path = _write_config(tmp_path)
    monkeypatch.setenv("MVP_INTERNAL_TOKEN", "admin-token")

    class _Scheduler:
        def __init__(self, **kwargs):
            self.tool = object()

        def run_forever(self, stop_flag):
            return None

    monkeypatch.setattr(app_mod, "Scheduler", _Scheduler)
    monkeypatch.setattr(app_mod, "new_model_key", lambda user_id: f"mk-{user_id}")
    app = app_mod.create_app(str(cfg_path))

    with TestClient(app) as c:
        # Create a user token
        admin_headers = {"authorization": "Bearer admin-token"}
        c.post("/api/v2/users", headers=admin_headers, json={"user_id": "alice"})
        token = c.post("/api/v2/users/alice/tokens", headers=admin_headers).json()["token"]
        headers = {"authorization": f"Bearer {token}"}

        r = c.post("/api/v2/serve/models", headers=headers, data=": bad\n")
        assert r.status_code == 400

        r2 = c.post("/api/v2/serve/models", headers=headers, data="- 1\n- 2\n")
        assert r2.status_code == 400


def test_serving_api_engine_kwargs_binary_rejected(tmp_path: Path, monkeypatch):
    """
    yaml !!binary is parsed as bytes, which is not JSON-serializable.
    """
    from argus.service import app as app_mod

    cfg_path = _write_config(tmp_path)
    monkeypatch.setenv("MVP_INTERNAL_TOKEN", "admin-token")

    class _Scheduler:
        def __init__(self, **kwargs):
            self.tool = object()

        def run_forever(self, stop_flag):
            return None

    monkeypatch.setattr(app_mod, "Scheduler", _Scheduler)
    monkeypatch.setattr(app_mod, "new_model_key", lambda user_id: f"mk-{user_id}")
    app = app_mod.create_app(str(cfg_path))

    admin_headers = {"authorization": "Bearer admin-token"}
    with TestClient(app) as c:
        c.post("/api/v2/users", headers=admin_headers, json={"user_id": "alice"})
        token = c.post("/api/v2/users/alice/tokens", headers=admin_headers).json()["token"]
        headers = {"authorization": f"Bearer {token}"}

        spec_yaml = (
            "model_id: x\n"
            "model_source: $HOME/common/hf/x\n"
            "engine_kwargs:\n"
            "  blob: !!binary \"AQID\"\n"
        )
        r = c.post("/api/v2/serve/models", headers=headers, data=spec_yaml)
        assert r.status_code == 400


def test_serving_api_list_include_deleted_and_forwarded_base_url(tmp_path: Path, monkeypatch):
    from argus.service import app as app_mod
    from argus.service.config import V2Config
    from argus.service.db import Db

    cfg_path = _write_config(tmp_path)
    monkeypatch.setenv("MVP_INTERNAL_TOKEN", "admin-token")

    class _Scheduler:
        def __init__(self, **kwargs):
            self.tool = object()

        def run_forever(self, stop_flag):
            return None

    monkeypatch.setattr(app_mod, "Scheduler", _Scheduler)
    keys = iter(["mk-alice-1", "mk-alice-2"])
    monkeypatch.setattr(app_mod, "new_model_key", lambda user_id: next(keys))

    app = app_mod.create_app(str(cfg_path))

    admin_headers = {"authorization": "Bearer admin-token"}
    with TestClient(app) as c:
        c.post("/api/v2/users", headers=admin_headers, json={"user_id": "alice"})
        token = c.post("/api/v2/users/alice/tokens", headers=admin_headers).json()["token"]
        headers = {"authorization": f"Bearer {token}"}

        spec_yaml = "model_id: x\nmodel_source: $HOME/common/hf/x\n"
        c.post("/api/v2/serve/models", headers=headers, data=spec_yaml)
        c.post("/api/v2/serve/models", headers=headers, data=spec_yaml)

        # Mark one model as DELETED directly in DB (sets deleted_at).
        root = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
        v2_cfg = V2Config.from_root_dict(root)
        db = Db(v2_cfg.sqlite.db_path)
        db.set_serve_model_state(model_key="mk-alice-2", state="DELETED")

        r1 = c.get(
            "/api/v2/serve/models?limit=10&offset=0&include_deleted=0",
            headers={**headers, "x-forwarded-host": "example.com:8080", "x-forwarded-proto": "https"},
        )
        assert r1.status_code == 200
        assert r1.json()["openai_base_url"] is None
        assert {m["model_key"] for m in r1.json()["items"]} == {"mk-alice-1"}
        assert r1.json()["items"][0]["endpoint"]["openai_base_url"] == "https://example.com:8000/serve/mk-alice-1/v1"

        # IPv6 bracketed host should be preserved.
        r1b = c.get(
            "/api/v2/serve/models?limit=10&offset=0&include_deleted=0",
            headers={**headers, "x-forwarded-host": "[::1]:8080", "x-forwarded-proto": "https"},
        )
        assert r1b.status_code == 200
        assert r1b.json()["items"][0]["endpoint"]["openai_base_url"] == "https://[::1]:8000/serve/mk-alice-1/v1"

        r2 = c.get("/api/v2/serve/models?include_deleted=1", headers=headers)
        assert r2.status_code == 200
        assert {m["model_key"] for m in r2.json()["items"]} == {"mk-alice-1", "mk-alice-2"}


def test_serving_api_patch_invalid_num_replicas(tmp_path: Path, monkeypatch):
    from argus.service import app as app_mod

    cfg_path = _write_config(tmp_path)
    monkeypatch.setenv("MVP_INTERNAL_TOKEN", "admin-token")

    class _Scheduler:
        def __init__(self, **kwargs):
            self.tool = object()

        def run_forever(self, stop_flag):
            return None

    monkeypatch.setattr(app_mod, "Scheduler", _Scheduler)
    monkeypatch.setattr(app_mod, "new_model_key", lambda user_id: "mk-alice")

    app = app_mod.create_app(str(cfg_path))

    admin_headers = {"authorization": "Bearer admin-token"}
    with TestClient(app) as c:
        c.post("/api/v2/users", headers=admin_headers, json={"user_id": "alice"})
        token = c.post("/api/v2/users/alice/tokens", headers=admin_headers).json()["token"]
        headers = {"authorization": f"Bearer {token}"}

        c.post("/api/v2/serve/models", headers=headers, data="model_id: x\nmodel_source: $HOME/common/hf/x\n")
        r = c.patch("/api/v2/serve/models/mk-alice", headers=headers, json={"num_replicas": 0})
        assert r.status_code == 422
