from __future__ import annotations

from collections.abc import Generator
from pathlib import Path
from unittest.mock import patch

import pygit2
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pygit2 import Oid

from pygittools.tasks import BOARD_REF_PREFIX, TASK_REF_PREFIX, Board, Comment, Task, get_board
from pygitweb.config import settings
from pygitweb.tasks import EMPTY_TREE_OID, board_router, comment_router, task_router


def _build_client() -> TestClient:
	app = FastAPI()
	app.include_router(board_router, prefix="/board")
	app.include_router(task_router, prefix="/tasks")
	app.include_router(comment_router, prefix="/comments")
	return TestClient(app)


def _create_initial_commit(repo: pygit2.Repository, repo_dir: Path) -> None:
	(repo_dir / "README.md").write_text("seed\n", encoding="utf-8")
	index = repo.index
	index.add("README.md")
	index.write()
	tree = index.write_tree()
	sig = pygit2.Signature("tester", "tester@example.com")
	repo.create_commit("HEAD", sig, sig, "initial", tree, [])


class TestTaskRoutes:
	class TestReadonlyResponses:
		@pytest.fixture(scope="class")
		def seeded_env(
			self, tmp_path_factory: pytest.TempPathFactory
		) -> Generator[dict[str, str | TestClient], None, None]:
			root: Path = tmp_path_factory.mktemp("tasks-routes-readonly")
			repo_dir: Path = root / "demo"
			repo = pygit2.init_repository(str(repo_dir), bare=False)
			project: str = "demo"

			board_ref: str = f"{BOARD_REF_PREFIX}Tasks"
			board = Board(EMPTY_TREE_OID, board_ref, tagger="", description="Main board")

			task_a_ref: str = f"{TASK_REF_PREFIX}task_a"
			task_a = Task(
				EMPTY_TREE_OID,
				task_a_ref,
				tagger="",
				title="Alpha",
				description="Alpha description",
				status=Task.Status.TODO,
				priority=Task.Priority.HIGH,
				assignee="alice",
			)
			task_a_oid = task_a.write(repo)
			comment_a = Comment(task_a_oid, "alice <alice@example.com>", "alpha note")
			comment_a_oid = comment_a.write(repo)
			task_a.comments = [str(comment_a_oid)]
			task_a.update_message()
			task_a_oid = task_a.write(repo)

			task_b_ref: str = f"{TASK_REF_PREFIX}task_b"
			task_b = Task(
				EMPTY_TREE_OID,
				task_b_ref,
				tagger="",
				title="Bravo",
				description="",
				status=Task.Status.IN_PROGRESS,
				priority=Task.Priority.MEDIUM,
				assignee=None,
			)
			task_b_oid = task_b.write(repo)

			board.tasks = [str(task_a_oid), str(task_b_oid)]
			board.update_message()
			board.write(repo)

			with (
				patch.object(settings, "PROJECTROOT", str(root)),
				patch.object(settings, "PROJECTS_LIST", str(root)),
				patch.object(settings, "STRICT_EXPORT", False),
				patch.object(settings, "EXPORT_OK", ""),
				patch.object(settings, "AUTH", False),
			):
				client = _build_client()
				yield {
					"client": client,
					"project": project,
					"board": "Tasks",
					"task_a_ref": task_a_ref,
					"task_b_ref": task_b_ref,
				}

		def test_board_list_response_format(self, seeded_env: dict[str, str | TestClient]) -> None:
			client = seeded_env["client"]
			assert isinstance(client, TestClient)
			resp = client.get("/board/list", params={"project": seeded_env["project"]})
			assert resp.status_code == 200
			body = resp.json()
			assert isinstance(body, list)
			assert body == [{"name": "Tasks", "description": "Main board", "task_count": 2}]

		def test_task_list_response_format(self, seeded_env: dict[str, str | TestClient]) -> None:
			client = seeded_env["client"]
			assert isinstance(client, TestClient)
			resp = client.get(
				"/tasks/list",
				params={"project": seeded_env["project"], "board": seeded_env["board"]},
			)
			assert resp.status_code == 200
			body = resp.json()
			assert isinstance(body, list)
			tasks_by_ref = {entry["ref"]: entry for entry in body}
			alpha = tasks_by_ref[str(seeded_env["task_a_ref"])]
			assert alpha["title"] == "Alpha"
			assert alpha["description"] == "Alpha description"
			assert alpha["status"] == "TODO"
			assert alpha["priority"] == "HIGH"
			assert alpha["assignee"] == "alice"
			assert alpha["comments_count"] == 1
			assert alpha["created_at"] is not None
			assert alpha["updated_at"] is not None

		def test_comment_list_response_format(self, seeded_env: dict[str, str | TestClient]) -> None:
			client = seeded_env["client"]
			assert isinstance(client, TestClient)
			resp = client.get(
				"/comments/list",
				params={
					"project": seeded_env["project"],
					"board": seeded_env["board"],
					"task": seeded_env["task_a_ref"],
				},
			)
			assert resp.status_code == 200
			body = resp.json()
			assert isinstance(body, list)
			assert len(body) == 1
			comment = body[0]
			assert comment["content"] == "alpha note"
			assert comment["tagger"] == "alice <alice@example.com>"
			assert comment["author"] == "alice"
			assert comment["gravatar_url"] is not None
			assert comment["created_at"] is not None
			assert comment["edited_at"] is not None

	class TestMutableResponses:
		@pytest.fixture(scope="class")
		def mutable_env(
			self, tmp_path_factory: pytest.TempPathFactory
		) -> Generator[dict[str, str | TestClient], None, None]:
			root: Path = tmp_path_factory.mktemp("tasks-routes-mutable")
			repo_dir: Path = root / "demo"
			repo = pygit2.init_repository(str(repo_dir), bare=False)
			_create_initial_commit(repo, repo_dir)
			project: str = "demo"
			board_name: str = "Tasks"
			board_ref: str = f"{BOARD_REF_PREFIX}{board_name}"
			board = Board(EMPTY_TREE_OID, board_ref, tagger="", description="Mutable board")
			task_ref = f"{TASK_REF_PREFIX}mutable_seed"
			task = Task(
				EMPTY_TREE_OID,
				task_ref,
				tagger="",
				title="Mutable seed",
				description="",
				status=Task.Status.TODO,
				priority=Task.Priority.LOW,
			)
			task_oid = task.write(repo)
			comment = Comment(task_oid, "seed <seed@example.com>", "seed comment")
			comment_oid = comment.write(repo)
			task.comments = [str(comment_oid)]
			task.update_message()
			task_oid = task.write(repo)
			board.tasks = [str(task_oid)]
			board.update_message()
			board.write(repo)

			with (
				patch.object(settings, "PROJECTROOT", str(root)),
				patch.object(settings, "PROJECTS_LIST", str(root)),
				patch.object(settings, "STRICT_EXPORT", False),
				patch.object(settings, "EXPORT_OK", ""),
				patch.object(settings, "AUTH", False),
			):
				client = _build_client()
				yield {
					"client": client,
					"project": project,
					"board": board_name,
					"seed_comment_oid": str(comment_oid),
				}

		def test_task_create_and_update_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
			client = mutable_env["client"]
			assert isinstance(client, TestClient)
			create_resp = client.post(
				"/tasks/create",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"title": "Created Task",
					"description": "Needs updates",
					"status": "TODO",
					"priority": "LOW",
				},
			)
			assert create_resp.status_code == 200
			created = create_resp.json()
			assert set(created.keys()) == {"task_id", "ref", "oid"}
			assert str(created["ref"]).startswith("refs/tags/tasks/task_")

			update_resp = client.post(
				"/tasks/update",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"task": created["ref"],
				},
				json={
					"title": "Updated Task",
					"description": "Updated description",
					"status": "IN_REVIEW",
					"priority": "CRITICAL",
					"assignee": "bob",
					"due_date": "2025-02-03T04:05:06+00:00",
				},
			)
			assert update_resp.status_code == 200
			updated = update_resp.json()
			assert updated["ref"] == created["ref"]
			assert updated["title"] == "Updated Task"
			assert updated["description"] == "Updated description"
			assert updated["status"] == "IN_REVIEW"
			assert updated["priority"] == "CRITICAL"
			assert updated["assignee"] == "bob"
			assert updated["due_date"] == "2025-02-03T04:05:06+00:00"

		def test_task_status_update_moves_oid_to_end_of_board_list(
			self, mutable_env: dict[str, str | TestClient]
		) -> None:
			client = mutable_env["client"]
			assert isinstance(client, TestClient)
			a_ref = client.post(
				"/tasks/create",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"title": "First",
					"status": "TODO",
				},
			).json()["ref"]
			b_ref = client.post(
				"/tasks/create",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"title": "Second",
					"status": "TODO",
				},
			).json()["ref"]
			list_before = client.get(
				"/tasks/list",
				params={"project": mutable_env["project"], "board": mutable_env["board"]},
			).json()
			refs_before = [t["ref"] for t in list_before if t["title"] in ("First", "Second")]
			assert refs_before.index(a_ref) < refs_before.index(b_ref)
			client.post(
				"/tasks/update",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"task": a_ref,
				},
				json={"status": "IN_PROGRESS"},
			)
			repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
			loaded = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
			assert loaded is not None
			a_oid = str(repo.references[a_ref].resolve().target)
			b_oid = str(repo.references[b_ref].resolve().target)
			assert loaded.tasks.index(b_oid) < loaded.tasks.index(a_oid)

		def test_task_list_resolves_stale_board_task_oid(self, mutable_env: dict[str, str | TestClient]) -> None:
			"""Board may still list an old task OID after the ref was rewritten."""
			client = mutable_env["client"]
			assert isinstance(client, TestClient)
			repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
			board = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
			assert board is not None
			stale_oid = board.tasks[0]
			task_ref = f"{TASK_REF_PREFIX}mutable_seed"
			t = Task(
				EMPTY_TREE_OID,
				task_ref,
				tagger="",
				title="Stale board entry",
				description="",
				status=Task.Status.TODO,
				priority=Task.Priority.LOW,
			)
			t.comments = ["abc123"]
			t.update_message()
			current_oid = t.write(repo)
			board.tasks = [stale_oid]
			board.update_message()
			board.write(repo)

			list_resp = client.get(
				"/tasks/list",
				params={"project": mutable_env["project"], "board": mutable_env["board"]},
			)
			assert list_resp.status_code == 200
			by_ref = {entry["ref"]: entry for entry in list_resp.json()}
			assert by_ref[task_ref]["comments_count"] == 1
			reloaded = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
			assert reloaded is not None
			assert str(current_oid) in reloaded.tasks

		def test_comment_create_updates_task_list_comments_count(
			self, mutable_env: dict[str, str | TestClient]
		) -> None:
			client = mutable_env["client"]
			assert isinstance(client, TestClient)
			create_resp = client.post(
				"/tasks/create",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"title": "Comment count task",
				},
			)
			assert create_resp.status_code == 200
			task_ref: str = create_resp.json()["ref"]
			comment_resp = client.post(
				"/comments/create",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"task": task_ref,
					"content": "board count note",
				},
			)
			assert comment_resp.status_code == 200
			assert comment_resp.json()["comments_count"] == 1
			list_resp = client.get(
				"/tasks/list",
				params={"project": mutable_env["project"], "board": mutable_env["board"]},
			)
			assert list_resp.status_code == 200
			by_ref = {entry["ref"]: entry for entry in list_resp.json()}
			assert by_ref[task_ref]["comments_count"] == 1
			repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
			board = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
			assert board is not None
			ref_oid = str(repo.references[task_ref].resolve().target)
			assert ref_oid in board.tasks

		def test_comment_delete_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
			client = mutable_env["client"]
			assert isinstance(client, TestClient)
			delete_comment_resp = client.post(
				"/comments/delete",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"comment": mutable_env["seed_comment_oid"],
				},
			)
			assert delete_comment_resp.status_code == 200
			assert delete_comment_resp.json() == {"message": "Comment deleted"}

		def test_task_delete_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
			client = mutable_env["client"]
			assert isinstance(client, TestClient)
			create_resp = client.post(
				"/tasks/create",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"title": "To delete",
				},
			)
			task_ref: str = create_resp.json()["ref"]
			delete_resp = client.post(
				"/tasks/delete",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"task": task_ref,
				},
			)
			assert delete_resp.status_code == 200
			assert delete_resp.json() == {"message": "Task deleted"}

			repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
			assert task_ref not in repo.references

		def test_board_create_and_delete_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
			client = mutable_env["client"]
			assert isinstance(client, TestClient)
			create_resp = client.post(
				"/board/create",
				params={
					"project": mutable_env["project"],
					"name": "temp-board",
					"description": "Temporary",
				},
			)
			assert create_resp.status_code == 200
			created = create_resp.json()
			assert set(created.keys()) == {"name", "ref", "oid"}
			assert created["name"] == "temp-board"
			assert created["ref"] == f"{BOARD_REF_PREFIX}temp-board"
			Oid(hex=created["oid"])

			delete_resp = client.post(
				"/board/delete",
				params={"project": mutable_env["project"], "name": "temp-board"},
			)
			assert delete_resp.status_code == 200
			assert delete_resp.json() == {"message": "Board deleted"}