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["id"] == "a"
			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_create_and_update_points_snap_to_fibonacci(
			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": "Pointed task",
					"points": "5",
				},
			)
			assert create_resp.status_code == 200
			task_ref = create_resp.json()["ref"]
			list_resp = client.get(
				"/tasks/list",
				params={"project": mutable_env["project"], "board": mutable_env["board"]},
			)
			by_ref = {entry["ref"]: entry for entry in list_resp.json()}
			assert by_ref[task_ref]["points"] == 5

			update_resp = client.post(
				"/tasks/update",
				params={"project": mutable_env["project"], "board": mutable_env["board"], "task": task_ref},
				json={"points": 4},
			)
			assert update_resp.status_code == 200
			assert update_resp.json()["points"] is None

			clear_resp = client.post(
				"/tasks/update",
				params={"project": mutable_env["project"], "board": mutable_env["board"], "task": task_ref},
				json={"points": None},
			)
			assert clear_resp.status_code == 200
			assert clear_resp.json()["points"] is None

		def test_task_move_transfers_task_between_boards(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": "Sprint-1"},
			)
			assert create_resp.status_code == 200
			task_ref = client.post(
				"/tasks/create",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"title": "Movable task",
				},
			).json()["ref"]

			move_resp = client.post(
				"/tasks/move",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"task": task_ref,
					"target": "Sprint-1",
				},
			)
			assert move_resp.status_code == 200
			assert move_resp.json() == {"message": "Task moved", "board": "Sprint-1"}

			source_list = client.get(
				"/tasks/list",
				params={"project": mutable_env["project"], "board": mutable_env["board"]},
			).json()
			assert task_ref not in [t["ref"] for t in source_list]

			target_list = client.get(
				"/tasks/list",
				params={"project": mutable_env["project"], "board": "Sprint-1"},
			).json()
			assert task_ref in [t["ref"] for t in target_list]

			client.post(
				"/board/delete",
				params={"project": mutable_env["project"], "name": "Sprint-1"},
			)

		def test_board_create_rejects_invalid_ref_name(self, mutable_env: dict[str, str | TestClient]) -> None:
			client = mutable_env["client"]
			assert isinstance(client, TestClient)
			resp = client.post(
				"/board/create",
				params={"project": mutable_env["project"], "name": "Sprint 1"},
			)
			assert resp.status_code == 400

		def test_task_move_to_missing_board_returns_404(self, mutable_env: dict[str, str | TestClient]) -> None:
			client = mutable_env["client"]
			assert isinstance(client, TestClient)
			task_ref = client.post(
				"/tasks/create",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"title": "Orphan candidate",
				},
			).json()["ref"]
			move_resp = client.post(
				"/tasks/move",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"task": task_ref,
					"target": "does-not-exist",
				},
			)
			assert move_resp.status_code == 404

		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_task_archive_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 archive",
				},
			)
			task_ref: str = create_resp.json()["ref"]
			archive_resp = client.post(
				"/tasks/archive",
				params={
					"project": mutable_env["project"],
					"board": mutable_env["board"],
					"task": task_ref,
				},
			)
			assert archive_resp.status_code == 200
			archived = archive_resp.json()
			assert archived["message"] == "Task archived"
			assert archived["archive_board"] == "Archive"
			assert archived["archive_ref"] == f"{BOARD_REF_PREFIX}Archive"

			repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
			assert task_ref in repo.references
			main_board = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
			assert main_board is not None
			main_oids = [str(o) for o in main_board.tasks]
			archived_oid = str(repo.references[task_ref].resolve().target)
			assert archived_oid not in main_oids
			archive_board = get_board(repo, f"{BOARD_REF_PREFIX}Archive")
			assert archive_board is not None
			assert len(archive_board.tasks) == 1

			list_resp = client.get(
				"/tasks/list",
				params={"project": mutable_env["project"], "board": mutable_env["board"]},
			)
			refs_on_main = [t["ref"] for t in list_resp.json()]
			assert task_ref not in refs_on_main

			archive_list_resp = client.get(
				"/tasks/list",
				params={"project": mutable_env["project"], "board": "Archive"},
			)
			refs_on_archive = [t["ref"] for t in archive_list_resp.json()]
			assert task_ref in refs_on_archive

		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"}