import asyncio
import sys
from pathlib import Path
from types import SimpleNamespace

from fastapi.testclient import TestClient

import mixersystem.studio.process_manager as process_manager_module
import mixersystem.studio.routes.linear as linear_routes
from mixersystem.studio.app import create_app
from mixersystem.studio.config import StudioConfig
from mixersystem.studio.event_bus import EventBus
from mixersystem.studio.process_manager import ProcessManager


def test_session_folder_rejects_path_traversal(tmp_path):
    (tmp_path / ".mixer" / "sessions").mkdir(parents=True)
    app = create_app(StudioConfig(project_root=tmp_path))

    with TestClient(app) as client:
        res = client.post("/api/v1/session-folders?name=../escape")
        assert res.status_code == 400
        assert res.json()["detail"]  # Any error message is fine

    assert not (tmp_path / ".mixer" / "escape").exists()


def test_workflow_start_rejects_invalid_session_folder(tmp_path):
    (tmp_path / ".mixer" / "sessions").mkdir(parents=True)
    app = create_app(StudioConfig(project_root=tmp_path))

    with TestClient(app) as client:
        res = client.post(
            "/api/v1/workflows/start",
            json={"workflow": "task", "session_folder": "../escape", "args": {}},
        )
        assert res.status_code == 400
        assert "Invalid session_folder" in res.json()["detail"]


def test_stop_emits_only_workflow_stopped(monkeypatch):
    async def _run() -> list[str]:
        monkeypatch.setattr(
            process_manager_module,
            "_build_cli_args",
            lambda *_args, **_kwargs: [sys.executable, "-c", "import time; time.sleep(5)"],
        )

        event_bus = EventBus()
        queue = event_bus.subscribe()
        pm = ProcessManager(event_bus, project_root=Path.cwd())

        run = await pm.start_workflow("task", ".mixer/sessions/stop-race", {})
        await asyncio.sleep(0.03)
        await pm.stop_workflow(run.run_id)
        await asyncio.sleep(0.05)

        types: list[str] = []
        while True:
            try:
                event = queue.get_nowait()
            except asyncio.QueueEmpty:
                break
            if event.type in {"workflow_finished", "workflow_stopped"}:
                types.append(event.type)

        await pm.shutdown()
        event_bus.unsubscribe(queue)
        return types

    types = asyncio.run(_run())
    assert types.count("workflow_stopped") == 1
    assert "workflow_finished" not in types


def test_stop_promotes_only_one_queued_run(monkeypatch):
    async def _run() -> int:
        monkeypatch.setattr(
            process_manager_module,
            "_build_cli_args",
            lambda *_args, **_kwargs: [sys.executable, "-c", "import time; time.sleep(10)"],
        )

        event_bus = EventBus()
        pm = ProcessManager(event_bus, project_root=Path.cwd())
        session = ".mixer/sessions/queue-race"

        first = await pm.start_workflow("task", session, {})
        await pm.start_workflow("plan", session, {})
        await pm.start_workflow("work", session, {})

        await asyncio.sleep(0.05)
        await pm.stop_workflow(first.run_id)
        await asyncio.sleep(0.2)

        running = [
            run
            for run in pm.list_runs()
            if run.session_folder == session and run.status == "running"
        ]

        await pm.shutdown()
        return len(running)

    running_count = asyncio.run(_run())
    assert running_count == 1


def test_concurrent_start_keeps_single_running(monkeypatch):
    async def _run() -> tuple[int, int]:
        monkeypatch.setattr(
            process_manager_module,
            "_build_cli_args",
            lambda *_args, **_kwargs: [sys.executable, "-c", "import time; time.sleep(5)"],
        )

        event_bus = EventBus()
        pm = ProcessManager(event_bus, project_root=Path.cwd())
        session = ".mixer/sessions/concurrent-start"

        async def launch(stage: str):
            return await pm.start_workflow(stage, session, {})

        await asyncio.gather(launch("task"), launch("plan"))
        await asyncio.sleep(0.1)

        runs = [r for r in pm.list_runs() if r.session_folder == session]
        running_count = sum(1 for r in runs if r.status == "running")
        queued_count = sum(1 for r in runs if r.status == "queued")

        await pm.shutdown()
        return running_count, queued_count

    running_count, queued_count = asyncio.run(_run())
    assert running_count == 1
    assert queued_count == 1


def test_large_stdout_does_not_block_completion(monkeypatch):
    async def _run() -> str:
        monkeypatch.setattr(
            process_manager_module,
            "_build_cli_args",
            lambda *_args, **_kwargs: [
                sys.executable,
                "-c",
                "import sys; sys.stdout.write('x'*5000000); sys.stdout.flush()",
            ],
        )

        event_bus = EventBus()
        pm = ProcessManager(event_bus, project_root=Path.cwd())
        run = await pm.start_workflow("task", ".mixer/sessions/stdout-drain", {})

        for _ in range(40):
            current = pm.get_run(run.run_id)
            if current and current.status in {"completed", "failed", "killed"}:
                break
            await asyncio.sleep(0.1)

        status = pm.get_run(run.run_id).status
        await pm.shutdown()
        return status

    status = asyncio.run(_run())
    assert status == "completed"


def test_linear_fetch_runtime_error_returns_502(monkeypatch, tmp_path):
    (tmp_path / ".mixer" / "sessions").mkdir(parents=True)
    monkeypatch.setenv("LINEAR_API_KEY", "test-key")

    linear_stub = SimpleNamespace(
        LOAD_QUERY="query",
        _graphql=lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")),
    )
    monkeypatch.setattr(linear_routes, "_linear_sync", linear_stub)

    app = create_app(StudioConfig(project_root=tmp_path))
    with TestClient(app, raise_server_exceptions=False) as client:
        res = client.get("/api/v1/linear/issues/ABC-123")
        assert res.status_code == 502
        assert "boom" in res.json()["detail"]
