"""
Tests for the Brynq Projects API.

Projects are workspaces where users organize agents and conversations
around a specific goal. Stored as JSON at ~/.brynq/projects.json.

All tests use tmp_path for storage — no real disk writes.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any
from unittest.mock import patch

import pytest

from runtime.config import RuntimeConfig


# ── Fixtures ──────────────────────────────────────────────────────────


@pytest.fixture()
def runtime_config(tmp_path: Path) -> RuntimeConfig:
    """Config with tmp_path as data directory."""
    return RuntimeConfig(
        cloud_url="https://mock.brynq.ai",
        data_dir=tmp_path,
        ollama_url="http://localhost:11434",
        debug=True,
    )


@pytest.fixture()
def client(runtime_config: RuntimeConfig) -> Any:
    """FastAPI TestClient for the runtime app."""
    from fastapi.testclient import TestClient

    from runtime.app import create_app

    app = create_app(runtime_config)
    return TestClient(app)


def _create_project(client: Any, name: str = "Test Project", description: str = "A test") -> dict:
    """Helper: create a project and return the response body."""
    resp = client.post("/projects", json={"name": name, "description": description})
    assert resp.status_code == 201
    return resp.json()


# ═══════════════════════════════════════════════════════════════════════
# Project Storage Helpers
# ═══════════════════════════════════════════════════════════════════════


class TestProjectStorage:
    """Tests for the file-based project storage functions."""

    def test_read_empty(self, tmp_path: Path) -> None:
        from runtime.app import _read_projects

        projects = _read_projects(tmp_path)
        assert projects == []

    def test_write_and_read(self, tmp_path: Path) -> None:
        from runtime.app import _read_projects, _write_projects

        projects = [{"id": "proj_test", "name": "Test"}]
        _write_projects(tmp_path, projects)
        loaded = _read_projects(tmp_path)
        assert len(loaded) == 1
        assert loaded[0]["id"] == "proj_test"

    def test_read_corrupt_json(self, tmp_path: Path) -> None:
        from runtime.app import _read_projects

        (tmp_path / "projects.json").write_text("not json!!!", "utf-8")
        projects = _read_projects(tmp_path)
        assert projects == []

    def test_find_project(self) -> None:
        from runtime.app import _find_project

        projects = [
            {"id": "proj_aaa", "name": "AAA"},
            {"id": "proj_bbb", "name": "BBB"},
        ]
        assert _find_project(projects, "proj_bbb")["name"] == "BBB"
        assert _find_project(projects, "proj_ccc") is None

    def test_file_location(self, tmp_path: Path) -> None:
        from runtime.app import _projects_file

        assert _projects_file(tmp_path) == tmp_path / "projects.json"


# ═══════════════════════════════════════════════════════════════════════
# POST /projects — Create
# ═══════════════════════════════════════════════════════════════════════


class TestCreateProject:
    """Tests for POST /projects."""

    def test_create_basic(self, client: Any) -> None:
        resp = client.post("/projects", json={
            "name": "Q4 Financial Analysis",
            "description": "Analyze Q4 revenue reports",
        })
        assert resp.status_code == 201
        data = resp.json()
        assert data["name"] == "Q4 Financial Analysis"
        assert data["description"] == "Analyze Q4 revenue reports"
        assert data["id"].startswith("proj_")
        assert len(data["id"]) > 5
        assert data["agents"] == []
        assert data["chat_ids"] == []
        assert "created_at" in data
        assert "updated_at" in data

    def test_create_no_description(self, client: Any) -> None:
        resp = client.post("/projects", json={"name": "Minimal Project"})
        assert resp.status_code == 201
        data = resp.json()
        assert data["name"] == "Minimal Project"
        assert data["description"] == ""

    def test_create_empty_name_rejected(self, client: Any) -> None:
        resp = client.post("/projects", json={"name": ""})
        assert resp.status_code == 400
        assert "name is required" in resp.json()["detail"]

    def test_create_missing_name_rejected(self, client: Any) -> None:
        resp = client.post("/projects", json={"description": "no name"})
        assert resp.status_code == 400

    def test_create_whitespace_name_rejected(self, client: Any) -> None:
        resp = client.post("/projects", json={"name": "   "})
        assert resp.status_code == 400

    def test_create_multiple(self, client: Any) -> None:
        _create_project(client, name="Project 1")
        _create_project(client, name="Project 2")
        _create_project(client, name="Project 3")

        resp = client.get("/projects")
        assert resp.json()["count"] == 3

    def test_create_unique_ids(self, client: Any) -> None:
        p1 = _create_project(client, name="A")
        p2 = _create_project(client, name="B")
        assert p1["id"] != p2["id"]


# ═══════════════════════════════════════════════════════════════════════
# GET /projects — List
# ═══════════════════════════════════════════════════════════════════════


class TestListProjects:
    """Tests for GET /projects."""

    def test_list_empty(self, client: Any) -> None:
        resp = client.get("/projects")
        assert resp.status_code == 200
        data = resp.json()
        assert data["projects"] == []
        assert data["count"] == 0

    def test_list_after_create(self, client: Any) -> None:
        _create_project(client, name="Project Alpha")
        resp = client.get("/projects")
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] == 1
        assert data["projects"][0]["name"] == "Project Alpha"


# ═══════════════════════════════════════════════════════════════════════
# GET /projects/{id} — Detail
# ═══════════════════════════════════════════════════════════════════════


class TestGetProject:
    """Tests for GET /projects/{id}."""

    def test_get_existing(self, client: Any) -> None:
        created = _create_project(client)
        resp = client.get(f"/projects/{created['id']}")
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == created["id"]
        assert data["name"] == created["name"]

    def test_get_not_found(self, client: Any) -> None:
        resp = client.get("/projects/proj_nonexistent")
        assert resp.status_code == 404
        assert "not found" in resp.json()["detail"]


# ═══════════════════════════════════════════════════════════════════════
# PUT /projects/{id} — Update
# ═══════════════════════════════════════════════════════════════════════


class TestUpdateProject:
    """Tests for PUT /projects/{id}."""

    def test_update_name(self, client: Any) -> None:
        created = _create_project(client, name="Old Name")
        resp = client.put(f"/projects/{created['id']}", json={"name": "New Name"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["name"] == "New Name"
        assert data["description"] == created["description"]

    def test_update_description(self, client: Any) -> None:
        created = _create_project(client)
        resp = client.put(
            f"/projects/{created['id']}",
            json={"description": "Updated description"},
        )
        assert resp.status_code == 200
        assert resp.json()["description"] == "Updated description"

    def test_update_agents(self, client: Any) -> None:
        created = _create_project(client)
        resp = client.put(
            f"/projects/{created['id']}",
            json={"agents": ["llama3", "claude"]},
        )
        assert resp.status_code == 200
        assert resp.json()["agents"] == ["llama3", "claude"]

    def test_update_multiple_fields(self, client: Any) -> None:
        created = _create_project(client, name="Old", description="Old desc")
        resp = client.put(
            f"/projects/{created['id']}",
            json={"name": "New", "description": "New desc"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["name"] == "New"
        assert data["description"] == "New desc"

    def test_update_sets_updated_at(self, client: Any) -> None:
        created = _create_project(client)
        original_updated = created["updated_at"]
        resp = client.put(f"/projects/{created['id']}", json={"name": "Changed"})
        data = resp.json()
        # updated_at should change (or at least be present)
        assert "updated_at" in data
        assert data["updated_at"] >= original_updated

    def test_update_empty_name_rejected(self, client: Any) -> None:
        created = _create_project(client)
        resp = client.put(f"/projects/{created['id']}", json={"name": ""})
        assert resp.status_code == 400
        assert "name cannot be empty" in resp.json()["detail"]

    def test_update_agents_not_list_rejected(self, client: Any) -> None:
        created = _create_project(client)
        resp = client.put(f"/projects/{created['id']}", json={"agents": "llama3"})
        assert resp.status_code == 400
        assert "agents must be a list" in resp.json()["detail"]

    def test_update_not_found(self, client: Any) -> None:
        resp = client.put("/projects/proj_ghost", json={"name": "Ghost"})
        assert resp.status_code == 404

    def test_update_persists(self, client: Any, runtime_config: RuntimeConfig) -> None:
        """Changes survive a re-read from disk."""
        created = _create_project(client, name="Original")
        client.put(f"/projects/{created['id']}", json={"name": "Persisted"})

        from runtime.app import _read_projects

        projects = _read_projects(runtime_config.data_dir)
        assert projects[0]["name"] == "Persisted"


# ═══════════════════════════════════════════════════════════════════════
# DELETE /projects/{id} — Delete
# ═══════════════════════════════════════════════════════════════════════


class TestDeleteProject:
    """Tests for DELETE /projects/{id}."""

    def test_delete_existing(self, client: Any) -> None:
        created = _create_project(client)
        resp = client.delete(f"/projects/{created['id']}")
        assert resp.status_code == 200
        data = resp.json()
        assert data["deleted"] is True
        assert data["id"] == created["id"]

        # Verify it's gone
        resp = client.get("/projects")
        assert resp.json()["count"] == 0

    def test_delete_not_found(self, client: Any) -> None:
        resp = client.delete("/projects/proj_nonexistent")
        assert resp.status_code == 404

    def test_delete_one_of_many(self, client: Any) -> None:
        p1 = _create_project(client, name="Keep")
        p2 = _create_project(client, name="Delete")
        p3 = _create_project(client, name="Also Keep")

        client.delete(f"/projects/{p2['id']}")

        resp = client.get("/projects")
        data = resp.json()
        assert data["count"] == 2
        names = {p["name"] for p in data["projects"]}
        assert names == {"Keep", "Also Keep"}


# ═══════════════════════════════════════════════════════════════════════
# POST /projects/{id}/agents — Add Agent
# ═══════════════════════════════════════════════════════════════════════


class TestAddAgentToProject:
    """Tests for POST /projects/{id}/agents."""

    def test_add_agent(self, client: Any) -> None:
        created = _create_project(client)
        resp = client.post(
            f"/projects/{created['id']}/agents",
            json={"model": "llama3"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["added"] is True
        assert data["model"] == "llama3"
        assert "llama3" in data["agents"]

    def test_add_multiple_agents(self, client: Any) -> None:
        created = _create_project(client)
        client.post(f"/projects/{created['id']}/agents", json={"model": "llama3"})
        client.post(f"/projects/{created['id']}/agents", json={"model": "claude"})
        client.post(f"/projects/{created['id']}/agents", json={"model": "gpt-4o"})

        resp = client.get(f"/projects/{created['id']}")
        assert resp.json()["agents"] == ["llama3", "claude", "gpt-4o"]

    def test_add_duplicate_agent_rejected(self, client: Any) -> None:
        created = _create_project(client)
        client.post(f"/projects/{created['id']}/agents", json={"model": "llama3"})
        resp = client.post(f"/projects/{created['id']}/agents", json={"model": "llama3"})
        assert resp.status_code == 409
        assert "already in project" in resp.json()["detail"]

    def test_add_agent_missing_model(self, client: Any) -> None:
        created = _create_project(client)
        resp = client.post(f"/projects/{created['id']}/agents", json={})
        assert resp.status_code == 400
        assert "model is required" in resp.json()["detail"]

    def test_add_agent_empty_model(self, client: Any) -> None:
        created = _create_project(client)
        resp = client.post(f"/projects/{created['id']}/agents", json={"model": ""})
        assert resp.status_code == 400

    def test_add_agent_project_not_found(self, client: Any) -> None:
        resp = client.post("/projects/proj_ghost/agents", json={"model": "llama3"})
        assert resp.status_code == 404

    def test_add_agent_updates_timestamp(self, client: Any) -> None:
        created = _create_project(client)
        original = created["updated_at"]
        client.post(f"/projects/{created['id']}/agents", json={"model": "llama3"})
        resp = client.get(f"/projects/{created['id']}")
        assert resp.json()["updated_at"] >= original


# ═══════════════════════════════════════════════════════════════════════
# DELETE /projects/{id}/agents/{model} — Remove Agent
# ═══════════════════════════════════════════════════════════════════════


class TestRemoveAgentFromProject:
    """Tests for DELETE /projects/{id}/agents/{model}."""

    def test_remove_agent(self, client: Any) -> None:
        created = _create_project(client)
        client.post(f"/projects/{created['id']}/agents", json={"model": "llama3"})
        client.post(f"/projects/{created['id']}/agents", json={"model": "claude"})

        resp = client.delete(f"/projects/{created['id']}/agents/llama3")
        assert resp.status_code == 200
        data = resp.json()
        assert data["removed"] is True
        assert data["model"] == "llama3"
        assert data["agents"] == ["claude"]

    def test_remove_agent_not_in_project(self, client: Any) -> None:
        created = _create_project(client)
        resp = client.delete(f"/projects/{created['id']}/agents/llama3")
        assert resp.status_code == 404
        assert "not in project" in resp.json()["detail"]

    def test_remove_agent_project_not_found(self, client: Any) -> None:
        resp = client.delete("/projects/proj_ghost/agents/llama3")
        assert resp.status_code == 404

    def test_remove_agent_updates_timestamp(self, client: Any) -> None:
        created = _create_project(client)
        client.post(f"/projects/{created['id']}/agents", json={"model": "llama3"})
        before = client.get(f"/projects/{created['id']}").json()["updated_at"]
        client.delete(f"/projects/{created['id']}/agents/llama3")
        after = client.get(f"/projects/{created['id']}").json()["updated_at"]
        assert after >= before


# ═══════════════════════════════════════════════════════════════════════
# End-to-End Workflow
# ═══════════════════════════════════════════════════════════════════════


class TestProjectWorkflow:
    """Full lifecycle: create, configure, use, delete."""

    def test_full_lifecycle(self, client: Any) -> None:
        # 1. Create
        project = _create_project(client, name="Research", description="Deep research project")
        pid = project["id"]

        # 2. Add agents
        client.post(f"/projects/{pid}/agents", json={"model": "llama3"})
        client.post(f"/projects/{pid}/agents", json={"model": "claude"})

        # 3. Verify state
        resp = client.get(f"/projects/{pid}")
        data = resp.json()
        assert data["name"] == "Research"
        assert data["agents"] == ["llama3", "claude"]

        # 4. Update
        client.put(f"/projects/{pid}", json={"name": "Research v2"})

        # 5. Remove an agent
        client.delete(f"/projects/{pid}/agents/llama3")

        # 6. Verify final state
        resp = client.get(f"/projects/{pid}")
        data = resp.json()
        assert data["name"] == "Research v2"
        assert data["agents"] == ["claude"]

        # 7. Delete
        resp = client.delete(f"/projects/{pid}")
        assert resp.json()["deleted"] is True

        # 8. Verify gone
        resp = client.get(f"/projects/{pid}")
        assert resp.status_code == 404

    def test_projects_persist_to_disk(self, client: Any, runtime_config: RuntimeConfig) -> None:
        """Projects survive a fresh read from the JSON file."""
        _create_project(client, name="Persistent")

        projects_file = runtime_config.data_dir / "projects.json"
        assert projects_file.exists()

        raw = json.loads(projects_file.read_text("utf-8"))
        assert len(raw["projects"]) == 1
        assert raw["projects"][0]["name"] == "Persistent"
