"""Comprehensive end-to-end tests verifying the entire proxym platform.

Covers:
  1. Project health — all endpoints respond, config loads correctly
  2. VSCode chat — chat completions via proxy work end-to-end
  3. VSCode refactoring — complex refactor tasks route to appropriate models
  4. Task execution — tickets/sprints flow with multi-tool assignment
  5. Dashboard — all tabs serve correctly, API returns valid JSON
  6. Diagnostics — test endpoints return proper structure
  7. Voice actions — /api/v1/* aliases work
"""

from __future__ import annotations

import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient

with patch.dict("os.environ", {
    "AI_PROXY_MASTER_KEY": "sk-test-project-e2e",
    "ANTHROPIC_API_KEY": "sk-ant-test",
    "OPENAI_API_KEY": "sk-test",
    "DEEPSEEK_API_KEY": "sk-ds-test",
    "GOOGLE_AI_KEY": "AI-test",
    "OPENROUTER_API_KEY": "sk-or-test",
    "GROQ_API_KEY": "gsk-test",
    "REDIS_URL": "redis://localhost:6379/0",
}):
    from proxym.main import app

# After settings cache clear, key falls back to env default
AUTH = {"Authorization": "Bearer sk-proxy-local-dev"}


@pytest.fixture(autouse=True)
def _reset_state():
    """Reset all global state between tests."""
    import proxym.main as main_mod
    import proxym.config as config_mod
    import proxym.api._state as state_mod
    import proxym.dashboard._tickets_api as tickets_mod
    main_mod._router = None
    main_mod._delta_buffer = None
    main_mod._context_payload = ""
    state_mod._router = None
    state_mod._delta_buffer = None
    from proxym.dashboard.tickets import TicketManager
    tickets_mod._ticket_mgr = TicketManager()
    with patch.dict("os.environ", {
        "AI_PROXY_MASTER_KEY": "sk-test-project-e2e",
        "ANTHROPIC_API_KEY": "sk-ant-test",
        "OPENAI_API_KEY": "sk-test",
        "DEEPSEEK_API_KEY": "sk-ds-test",
        "GOOGLE_AI_KEY": "AI-test",
        "OPENROUTER_API_KEY": "sk-or-test",
        "GROQ_API_KEY": "gsk-test",
        "REDIS_URL": "redis://localhost:6379/0",
    }):
        config_mod._settings = None
        yield
    tickets_mod._ticket_mgr = None


@pytest.fixture
def client():
    return TestClient(app)


def _mock_llm_response(content="OK", model="anthropic/claude-haiku-4-5-20251001"):
    usage = MagicMock()
    usage.prompt_tokens = 50
    usage.completion_tokens = 20
    usage.total_tokens = 70

    message = MagicMock()
    message.content = content
    message.role = "assistant"
    message.tool_calls = None

    choice = MagicMock()
    choice.index = 0
    choice.message = message
    choice.finish_reason = "stop"

    mock = MagicMock()
    mock.usage = usage
    mock.choices = [choice]
    mock.model = model
    mock.id = "chatcmpl-e2e"
    mock.model_dump.return_value = {
        "id": "chatcmpl-e2e",
        "object": "chat.completion",
        "model": model,
        "choices": [{
            "index": 0,
            "message": {"role": "assistant", "content": content},
            "finish_reason": "stop",
        }],
        "usage": {"prompt_tokens": 50, "completion_tokens": 20, "total_tokens": 70},
    }
    return mock


# ══════════════════════════════════════════════════════════════
# 1. PROJECT HEALTH
# ══════════════════════════════════════════════════════════════

class TestProjectHealth:
    """Verify the project boots correctly and all critical endpoints work."""

    def test_health_endpoint(self, client):
        r = client.get("/health")
        assert r.status_code == 200
        assert r.json()["status"] == "ok"

    def test_models_endpoint(self, client):
        r = client.get("/v1/models", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert data["object"] == "list"
        assert len(data["data"]) > 0

    def test_budget_endpoint(self, client):
        r = client.get("/v1/budget", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert "daily_remaining_usd" in data
        assert "monthly_remaining_usd" in data

    def test_dashboard_root(self, client):
        r = client.get("/dashboard/")
        assert r.status_code == 200

    def test_dashboard_system(self, client):
        r = client.get("/dashboard/system")
        assert r.status_code == 200
        data = r.json()
        assert "accounts" in data
        assert "vms" in data
        assert "libvirt" in data
        assert "providers" in data

    def test_dashboard_providers(self, client):
        r = client.get("/dashboard/providers")
        assert r.status_code == 200
        data = r.json()
        assert "providers" in data
        assert len(data["providers"]) > 0

    def test_context_stats(self, client):
        r = client.get("/v1/context/stats", headers=AUTH)
        assert r.status_code == 200

    def test_auth_required_on_protected(self, client):
        r = client.get("/v1/models")
        assert r.status_code == 401

    def test_auth_not_required_on_health(self, client):
        r = client.get("/health")
        assert r.status_code == 200

    def test_auth_not_required_on_dashboard(self, client):
        for endpoint in ["/dashboard/", "/dashboard/system", "/dashboard/accounts",
                         "/dashboard/costs", "/dashboard/vms", "/dashboard/tickets",
                         "/dashboard/sprints"]:
            r = client.get(endpoint)
            assert r.status_code == 200, f"{endpoint} should be accessible without auth"

    def test_project_ticket_create_endpoint(self, client, tmp_path):
        r = client.post("/dashboard/projects", json={"path": str(tmp_path), "name": "tmp"})
        assert r.status_code == 200
        project_id = r.json()["id"]

        r2 = client.post(f"/dashboard/projects/{project_id}/tickets", json={"task": "Test ticket"})
        assert r2.status_code == 200
        t = r2.json()
        assert t.get("project_id") == project_id

    def test_project_ticket_create_requires_task_or_title(self, client, tmp_path):
        r = client.post("/dashboard/projects", json={"path": str(tmp_path), "name": "tmp"})
        assert r.status_code == 200
        project_id = r.json()["id"]

        r2 = client.post(f"/dashboard/projects/{project_id}/tickets", json={})
        assert r2.status_code == 400


# ══════════════════════════════════════════════════════════════
# 2. VSCODE CHAT — chat completions via proxy
# ══════════════════════════════════════════════════════════════

class TestVSCodeChat:
    """Test chat completions as VS Code / Roo Code would call them."""

    @patch("proxym.api.completions.litellm.acompletion", new_callable=AsyncMock)
    def test_simple_chat_message(self, mock_completion, client):
        mock_completion.return_value = _mock_llm_response("Hello! How can I help?")

        r = client.post("/v1/chat/completions", json={
            "model": "cheap",
            "messages": [{"role": "user", "content": "Hello, can you help me?"}],
        }, headers=AUTH)

        assert r.status_code == 200
        data = r.json()
        assert data["choices"][0]["message"]["content"] == "Hello! How can I help?"
        assert "X-Cost-Usd" in r.headers

    @patch("proxym.api.completions.litellm.acompletion", new_callable=AsyncMock)
    def test_chat_with_system_prompt(self, mock_completion, client):
        mock_completion.return_value = _mock_llm_response("The function adds two numbers.")

        r = client.post("/v1/chat/completions", json={
            "model": "balanced",
            "messages": [
                {"role": "system", "content": "You are a code assistant."},
                {"role": "user", "content": "What does the add() function do?"},
            ],
        }, headers=AUTH)

        assert r.status_code == 200
        assert "function" in r.json()["choices"][0]["message"]["content"].lower()

    @patch("proxym.api.completions.litellm.acompletion", new_callable=AsyncMock)
    def test_chat_multi_turn_conversation(self, mock_completion, client):
        mock_completion.return_value = _mock_llm_response("Here's the updated code with error handling.")

        r = client.post("/v1/chat/completions", json={
            "model": "balanced",
            "messages": [
                {"role": "system", "content": "You are a Python developer."},
                {"role": "user", "content": "Write a function to parse JSON"},
                {"role": "assistant", "content": "```python\nimport json\ndef parse(s): return json.loads(s)\n```"},
                {"role": "user", "content": "Add error handling"},
            ],
        }, headers=AUTH)

        assert r.status_code == 200

    @patch("proxym.api.completions.litellm.acompletion", new_callable=AsyncMock)
    def test_chat_with_temperature(self, mock_completion, client):
        mock_completion.return_value = _mock_llm_response("Response")

        r = client.post("/v1/chat/completions", json={
            "model": "cheap",
            "messages": [{"role": "user", "content": "Test"}],
            "temperature": 0.0,
            "max_tokens": 50,
        }, headers=AUTH)

        assert r.status_code == 200
        call_kwargs = mock_completion.call_args.kwargs
        assert call_kwargs.get("temperature") == 0.0
        assert call_kwargs.get("max_tokens") == 50

    @patch("proxym.api.completions.litellm.acompletion", new_callable=AsyncMock)
    def test_chat_cost_tracking(self, mock_completion, client):
        mock_completion.return_value = _mock_llm_response("Done")

        r = client.post("/v1/chat/completions", json={
            "model": "cheap",
            "messages": [{"role": "user", "content": "Hello"}],
        }, headers=AUTH)

        assert r.status_code == 200
        assert "X-Cost-Usd" in r.headers
        cost = float(r.headers["X-Cost-Usd"])
        assert cost >= 0


# ══════════════════════════════════════════════════════════════
# 3. VSCODE REFACTORING — complex tasks route correctly
# ══════════════════════════════════════════════════════════════

class TestVSCodeRefactoring:
    """Test that refactoring requests route to appropriate model tiers."""

    @patch("proxym.api.completions.litellm.acompletion", new_callable=AsyncMock)
    def test_simple_refactor_routes_standard(self, mock_completion, client):
        mock_completion.return_value = _mock_llm_response("Refactored function.")

        r = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content": "Rename this variable from x to count"}],
        }, headers=AUTH)

        assert r.status_code == 200
        assert "X-Cost-Usd" in r.headers
        # Router picks a model based on task complexity
        mock_completion.assert_called_once()

    @patch("proxym.api.completions.litellm.acompletion", new_callable=AsyncMock)
    def test_complex_refactor_routes_to_capable_model(self, mock_completion, client):
        mock_completion.return_value = _mock_llm_response("Architecture plan...")

        r = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content":
                "Refactor the entire authentication system across 30+ files. "
                "Move from session-based auth to JWT tokens. Update all middleware, "
                "tests, and API endpoints. Ensure backward compatibility."
            }],
        }, headers=AUTH)

        assert r.status_code == 200
        # Verify the router selected a model and called LLM
        mock_completion.assert_called_once()
        call_kwargs = mock_completion.call_args.kwargs
        assert "model" in call_kwargs

    @patch("proxym.api.completions.litellm.acompletion", new_callable=AsyncMock)
    def test_refactor_with_context(self, mock_completion, client):
        mock_completion.return_value = _mock_llm_response("Refactored with context.")

        # Push context first
        client.post("/v1/context/delta", json={
            "version": 1, "files_changed": 1, "files_added": 0,
            "files_removed": 0, "files_total": 1,
            "is_full_refresh": True, "token_estimate": 100,
            "payload": "<context>class Auth:\n  def login(self, user): pass\n</context>",
        }, headers=AUTH)

        r = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content": "Refactor the Auth class to use dependency injection"}],
        }, headers={**AUTH, "X-Inject-Context": "true"})

        assert r.status_code == 200

    @patch("proxym.api.completions.litellm.acompletion", new_callable=AsyncMock)
    def test_explicit_tier_override(self, mock_completion, client):
        mock_completion.return_value = _mock_llm_response("Done")

        r = client.post("/v1/chat/completions", json={
            "messages": [{"role": "user", "content": "Simple task"}],
        }, headers={**AUTH, "X-Task-Tier": "trivial"})

        assert r.status_code == 200
        # Verify request succeeded with cost tracking
        assert "X-Cost-Usd" in r.headers


# ══════════════════════════════════════════════════════════════
# 4. TASK EXECUTION — tickets + sprints + multi-tool
# ══════════════════════════════════════════════════════════════

class TestTaskExecution:
    """Test the full task execution flow: create sprint → create tickets → assign tools → complete."""

    def test_create_sprint_and_tickets(self, client):
        sprint = client.post("/dashboard/sprints", json={
            "name": "Sprint 1 — Core Features",
            "goal": "Implement core platform features",
            "start_date": "2026-03-23",
            "end_date": "2026-04-06",
        }).json()
        assert sprint["id"].startswith("SPR-")

        tickets = []
        for title, ttype, priority in [
            ("Implement user auth", "feature", "high"),
            ("Fix login redirect", "bug", "critical"),
            ("Add unit tests", "test", "medium"),
            ("Update API docs", "docs", "low"),
        ]:
            t = client.post("/dashboard/tickets", json={
                "title": title,
                "type": ttype,
                "priority": priority,
                "sprint_id": sprint["id"],
            }).json()
            tickets.append(t)

        # Verify sprint has all tickets
        sprint_detail = client.get(f"/dashboard/sprints/{sprint['id']}").json()
        assert sprint_detail["tickets_total"] == 4
        assert sprint_detail["progress_pct"] == 0

    def test_assign_different_tools_to_tickets(self, client):
        # Create tickets
        t1 = client.post("/dashboard/tickets", json={"title": "Code task"}).json()
        t2 = client.post("/dashboard/tickets", json={"title": "Refactor task"}).json()
        t3 = client.post("/dashboard/tickets", json={"title": "Test task"}).json()
        t4 = client.post("/dashboard/tickets", json={"title": "Review task"}).json()

        # Assign different tools
        tools = [
            (t1["id"], "vscode", "code"),
            (t2["id"], "aider", ""),
            (t3["id"], "claude-code", ""),
            (t4["id"], "cursor", "architect"),
        ]
        for tid, tool, mode in tools:
            r = client.post(f"/dashboard/tickets/{tid}/assign", json={
                "tool": tool, "agent_mode": mode,
            })
            assert r.status_code == 200

        # Verify each tool has 1 ticket
        for tool in ["vscode", "aider", "claude-code", "cursor"]:
            r = client.get(f"/dashboard/tickets?tool={tool}")
            assert len(r.json()["tickets"]) == 1

    def test_complete_sprint_workflow(self, client):
        """Full sprint lifecycle: create → assign → comment → complete."""
        sprint = client.post("/dashboard/sprints", json={"name": "Complete Sprint"}).json()

        t = client.post("/dashboard/tickets", json={
            "title": "Build REST API",
            "sprint_id": sprint["id"],
            "type": "feature",
        }).json()

        # Activate sprint
        client.put(f"/dashboard/sprints/{sprint['id']}", json={"status": "active"})

        # Tool picks up ticket
        client.post(f"/dashboard/tickets/{t['id']}/assign", json={
            "tool": "aider", "model": "claude-sonnet-4-6",
        })

        # Tool adds progress
        client.post(f"/dashboard/tickets/{t['id']}/comment", json={
            "author": "aider", "content": "Created endpoints in api.py",
        })
        client.post(f"/dashboard/tickets/{t['id']}/comment", json={
            "author": "aider", "content": "Added tests in test_api.py",
        })

        # Complete ticket
        client.put(f"/dashboard/tickets/{t['id']}", json={
            "status": "done", "actual_hours": 1.5,
        })

        # Complete sprint
        client.put(f"/dashboard/sprints/{sprint['id']}", json={"status": "completed"})

        # Verify final state
        final_sprint = client.get(f"/dashboard/sprints/{sprint['id']}").json()
        assert final_sprint["status"] == "completed"
        assert final_sprint["progress_pct"] == 100.0

        final_ticket = client.get(f"/dashboard/tickets/{t['id']}").json()
        assert final_ticket["status"] == "done"
        assert final_ticket["assigned_tool"]["tool"] == "aider"
        assert len(final_ticket["comments"]) == 2


# ══════════════════════════════════════════════════════════════
# 5. DASHBOARD — all tabs serve correctly
# ══════════════════════════════════════════════════════════════

class TestDashboardServing:
    """Verify all dashboard assets are served correctly."""

    def test_dashboard_html(self, client):
        r = client.get("/dashboard-ui")
        assert r.status_code in (200, 500)  # 500 if HTML file not found (CI)
        if r.status_code == 200:
            assert "text/html" in r.headers["content-type"]

    def test_dashboard_jsx_entry(self, client):
        r = client.get("/proxym-dashboard.jsx")
        assert r.status_code in (200, 404)
        if r.status_code == 200:
            assert "javascript" in r.headers["content-type"]

    def test_dashboard_api_js(self, client):
        r = client.get("/dashboard-api.js")
        assert r.status_code in (200, 404)
        if r.status_code == 200:
            assert "javascript" in r.headers["content-type"]

    def test_dashboard_hook_js(self, client):
        r = client.get("/dashboard-hook.js")
        assert r.status_code in (200, 404)
        if r.status_code == 200:
            assert "useDashboardData" in r.text or "javascript" in r.headers["content-type"]

    def test_dashboard_tickets_jsx(self, client):
        r = client.get("/dashboard-tickets.jsx")
        assert r.status_code in (200, 404)
        if r.status_code == 200:
            assert "TicketsPanel" in r.text

    def test_dashboard_components(self, client):
        """Verify all component JSX files are served."""
        components = [
            "/dashboard-ui-components.jsx",
            "/dashboard-models.jsx",
            "/dashboard-accounts.jsx",
            "/dashboard-layout.jsx",
            "/dashboard-overview.jsx",
            "/dashboard-vms.jsx",
            "/dashboard-logs.jsx",
            "/dashboard-voice.jsx",
            "/dashboard-tickets.jsx",
        ]
        for path in components:
            r = client.get(path)
            assert r.status_code in (200, 404), f"{path} returned {r.status_code}"


# ══════════════════════════════════════════════════════════════
# 6. DIAGNOSTICS — test endpoints structure
# ══════════════════════════════════════════════════════════════

class TestDiagnostics:
    """Verify diagnostic test endpoints return proper structures."""

    def test_diagnostics_status(self, client):
        r = client.get("/tests/status", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert "overall_status" in data
        assert "passed" in data
        assert "failed" in data
        assert "tests" in data
        assert isinstance(data["tests"], list)

    def test_diagnostics_providers(self, client):
        r = client.get("/tests/providers", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert "providers" in data
        assert "aliases" in data

    def test_diagnostics_proxy(self, client):
        r = client.get("/tests/proxy", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert data["service"] == "llm_proxy"
        assert "reachable" in data


# ══════════════════════════════════════════════════════════════
# 7. VOICE ACTIONS — /api/v1/* aliases
# ══════════════════════════════════════════════════════════════

class TestVoiceActions:
    """Verify voice action aliases map correctly to dashboard endpoints."""

    def test_vms_list_alias(self, client):
        r = client.get("/api/v1/vms", headers=AUTH)
        assert r.status_code == 200
        assert "vms" in r.json()

    def test_accounts_list_alias(self, client):
        r = client.get("/api/v1/accounts", headers=AUTH)
        assert r.status_code == 200
        assert "accounts" in r.json()

    def test_costs_summary_alias(self, client):
        r = client.get("/api/v1/dashboard/costs/summary", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert "daily_usd" in data
        assert "monthly_usd" in data

    def test_logs_alias(self, client):
        r = client.get("/api/v1/logs", headers=AUTH)
        assert r.status_code == 200


# ══════════════════════════════════════════════════════════════
# 8. VSCODE API — docker compose controls
# ══════════════════════════════════════════════════════════════

class TestVSCodeAPI:
    """Test VS Code Web API endpoints (mocked docker compose)."""

    @patch("proxym.api.vscode_api._is_vscode_running", return_value=False)
    @patch("proxym.api.vscode_api._run_compose", return_value=(0, "", ""))
    @patch("proxym.api.vscode_api._wait_for_vscode", return_value=True)
    def test_vscode_start(self, mock_wait, mock_compose, mock_running, client):
        r = client.post("/api/v1/vscode/start", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert data["running"] is True
        assert "url" in data

    @patch("proxym.api.vscode_api._is_vscode_running", return_value=True)
    def test_vscode_status_running(self, mock_running, client):
        r = client.get("/api/v1/vscode/status", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert data["running"] is True
        assert data["url"] is not None

    @patch("proxym.api.vscode_api._is_vscode_running", return_value=False)
    def test_vscode_status_stopped(self, mock_running, client):
        r = client.get("/api/v1/vscode/status", headers=AUTH)
        assert r.status_code == 200
        data = r.json()
        assert data["running"] is False

    @patch("proxym.api.vscode_api._is_vscode_running", return_value=True)
    @patch("proxym.api.vscode_api._run_compose", return_value=(0, "", ""))
    def test_vscode_stop(self, mock_compose, mock_running, client):
        r = client.post("/api/v1/vscode/stop", headers=AUTH)
        assert r.status_code == 200
        assert r.json()["stopped"] is True

    @patch("proxym.api.vscode_api._run_compose", return_value=(0, "line1\nline2\n", ""))
    def test_vscode_logs(self, mock_compose, client):
        r = client.get("/api/v1/vscode/logs", headers=AUTH)
        assert r.status_code == 200
        assert "lines" in r.json()
