from __future__ import annotations

import json
from collections.abc import Callable
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from pygit2 import Commit, Oid, Repository, Signature
from pyratatui import Rect

from pygittools.tasks import BOARD_REF_PREFIX, EMPTY_TREE_OID_HEX, TASK_REF_PREFIX, Board, Task
from pygittools.tui.branches import current_branch_name
from pygittools.tui.keys import key
from pygittools.tui.pages.branches import BranchPickerPage, branch_picker_page
from pygittools.tui.pages.help import HelpPage, help_page
from pygittools.tui.pages.home import HomePage, home_page
from pygittools.tui.pages.no_repo import NoRepoPage
from pygittools.tui.pages.projects import ProjectsPage, projects_page
from pygittools.tui.pages.task_editor import TaskEditorPage
from pygittools.tui.types import PageAction, PageContext


def test_home_page_on_enter_and_quit(page_ctx: PageContext) -> None:
	page = home_page()
	page.on_enter(page_ctx)
	assert page.status_text()
	result = page.handle_key(key("q"))
	assert result.action == PageAction.QUIT


def test_home_page_draw_skips_refresh(page_ctx: PageContext) -> None:
	page = HomePage()
	page.on_enter(page_ctx)
	with (
		patch.object(page.changes, "refresh") as changes_refresh,
		patch.object(page.log, "refresh") as log_refresh,
		patch.object(page.tasks, "refresh") as tasks_refresh,
		patch.object(page.worktrees, "refresh") as worktrees_refresh,
	):
		page.draw(MagicMock(), Rect(0, 0, 80, 20))
	changes_refresh.assert_not_called()
	log_refresh.assert_not_called()
	tasks_refresh.assert_not_called()
	worktrees_refresh.assert_not_called()


def test_home_page_idle_refresh_after_interval(page_ctx: PageContext) -> None:
	page = HomePage()
	with patch("pygittools.tui.pages.home.time.monotonic", return_value=100.0):
		page.on_enter(page_ctx)
	with (
		patch.object(page.changes, "refresh") as changes_refresh,
		patch.object(page.log, "refresh") as log_refresh,
		patch.object(page.tasks, "refresh") as tasks_refresh,
		patch.object(page.worktrees, "refresh") as worktrees_refresh,
		patch("pygittools.tui.pages.home.time.monotonic", side_effect=[109.0, 110.0, 110.0]),
	):
		page.on_idle()
		changes_refresh.assert_not_called()
		page.on_idle()
	changes_refresh.assert_called_once_with(page_ctx.repo)
	log_refresh.assert_called_once_with(page_ctx.repo)
	tasks_refresh.assert_called_once_with(page_ctx.repo)
	worktrees_refresh.assert_called_once_with(page_ctx.repo)


def test_home_page_handle_key_refreshes(page_ctx: PageContext) -> None:
	page = HomePage()
	page.on_enter(page_ctx)
	with (
		patch.object(page.changes, "refresh") as changes_refresh,
		patch.object(page.log, "refresh") as log_refresh,
		patch.object(page.tasks, "refresh") as tasks_refresh,
		patch.object(page.worktrees, "refresh") as worktrees_refresh,
	):
		page.handle_key(key("j"))
	changes_refresh.assert_called_once_with(page_ctx.repo)
	log_refresh.assert_called_once_with(page_ctx.repo)
	tasks_refresh.assert_called_once_with(page_ctx.repo)
	worktrees_refresh.assert_called_once_with(page_ctx.repo)


def test_home_page_draw_and_tabs(committed_repo: Repository) -> None:
	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = home_page()
	page.on_enter(ctx)
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	page.set_active_tab("log")
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	page.set_active_tab("tasks")
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	page.set_active_tab("worktrees")
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	page.set_header_focus("repo")
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	page.handle_key(key("Null"))


def test_home_page_switch_to_input(page_ctx: PageContext) -> None:
	page = home_page()
	page.on_enter(page_ctx)
	page.handle_key(key("a"))
	assert "Esc" in page.status_text()


def test_home_page_types_q_in_commit_input(page_ctx: PageContext) -> None:
	page = HomePage()
	page.on_enter(page_ctx)
	page.handle_key(key("a"))
	result = page.handle_key(key("q"))
	assert result.action == PageAction.NONE
	assert "q" in page.changes.commit_message
	result = page.handle_key(key("Q"))
	assert result.action == PageAction.NONE
	assert "qQ" in page.changes.commit_message


def test_branch_filter_types_q(committed_repo: Repository) -> None:
	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = BranchPickerPage()
	page.on_enter(ctx)
	result = page.handle_key(key("q"))
	assert result.action == PageAction.NONE
	assert page._filter_state.value() == "q"
	result = page.handle_key(key("Q"))
	assert result.action == PageAction.NONE
	assert page._filter_state.value() == "qQ"


def test_task_editor_types_q_in_input(page_ctx: PageContext) -> None:
	page = TaskEditorPage(board_name="Tasks", task_ref=None, default_status="todo")
	page.on_enter(page_ctx)
	page.handle_key(key("Enter"))
	result = page.handle_key(key("q"))
	assert result.action == PageAction.NONE
	assert page._text_state.value() == "q"
	result = page.handle_key(key("Q"))
	assert result.action == PageAction.NONE
	assert page._text_state.value() == "qQ"


def test_home_page_push_help(page_ctx: PageContext) -> None:
	page = home_page()
	page.on_enter(page_ctx)
	result = page.handle_key(key("?"))
	assert result.action == PageAction.PUSH
	assert isinstance(result.next_page, HelpPage)


def test_home_page_draws_detached_head(committed_repo: Repository) -> None:
	commit = committed_repo.get(committed_repo.head.target)
	committed_repo.set_head(commit.id)
	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = home_page()
	page.on_enter(ctx)
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	assert current_branch_name(committed_repo) == "(detached)"
	assert "j/k move" in page.status_text()
	page.set_active_tab("log")
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	assert "No commits" not in page.status_text()


def test_empty_repo_can_create_branch_and_commit(repo: Repository) -> None:
	assert repo.head_is_unborn
	assert list(repo.branches.local) == []
	ctx = PageContext(repo=repo, repo_path=Path(repo.path))
	page = home_page()
	assert isinstance(page, HomePage)
	page.on_enter(ctx)
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	page.set_active_tab("log")
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	assert page.status_text() == "No commits — j/k move"
	page.set_active_tab("status")
	page.draw(MagicMock(), Rect(0, 0, 80, 20))

	page.handle_key(key("Up"))
	opened = page.handle_key(key("Enter"))
	assert opened.action == PageAction.PUSH
	assert isinstance(opened.next_page, BranchPickerPage)
	picker = opened.next_page
	picker.on_enter(ctx)
	picker.draw(MagicMock(), Rect(0, 0, 80, 20))
	for char in "feature":
		picker.handle_key(key(char))
	created = picker.handle_key(key("Enter"))
	assert created.action == PageAction.POP
	assert current_branch_name(repo) == "feature"

	workdir = repo.workdir
	assert workdir is not None
	(Path(workdir) / "hello.txt").write_text("hello\n", encoding="utf-8")
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	page.handle_key(key("Down"))
	_press_until(page, key("Down"), lambda hint: hint.startswith("Enter stage —"))
	page.handle_key(key("Enter"))
	_press_until(page, key("Up"), lambda hint: "type message" in hint.casefold())
	for char in "init":
		page.handle_key(key(char))
	page.handle_key(key("Enter"))

	assert not repo.head_is_unborn, page.changes.status_hint()
	assert current_branch_name(repo) == "feature"
	assert "feature" in repo.branches.local
	assert repo.head.peel(Commit).message.splitlines()[0] == "init"
	page.set_active_tab("log")
	page.draw(MagicMock(), Rect(0, 0, 80, 20))
	assert "No commits" not in page.status_text()


def _press_until(page: HomePage, press: object, ready: Callable[[str], bool]) -> None:
	for _ in range(12):
		hint = page.status_text()
		if ready(hint):
			return
		page.handle_key(press)  # type: ignore[arg-type]
	raise AssertionError(page.status_text())


def test_home_page_draw_without_context() -> None:
	page = home_page()
	page.draw(MagicMock(), Rect(0, 0, 80, 10))


def test_no_repo_page(tmp_path: Path) -> None:
	page = NoRepoPage(tmp_path)
	page.on_enter(PageContext(repo=MagicMock(), repo_path=tmp_path))  # type: ignore[arg-type]
	page.draw(MagicMock(), Rect(0, 0, 80, 10))
	assert page.handle_key(key("q")).action == PageAction.QUIT
	assert page.handle_key(key("h")).action == PageAction.PUSH
	page.handle_key(key("Null"))


def test_help_page_scroll_and_pop() -> None:
	page = help_page()
	page.on_enter(PageContext(repo=MagicMock(), repo_path=Path(".")))  # type: ignore[arg-type]
	page.draw(MagicMock(), Rect(0, 0, 80, 5))
	page.handle_key(key("Down"))
	page.handle_key(key("Up"))
	assert page.handle_key(key("q")).action == PageAction.POP
	page.handle_key(key("Null"))


def test_branch_picker_lists_and_filters(committed_repo: Repository) -> None:
	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = branch_picker_page()
	page.on_enter(ctx)
	page.draw(MagicMock(), Rect(0, 0, 80, 10))
	page.handle_key(key("f"))
	page.handle_key(key("Backspace"))
	page.handle_key(key("Down"))
	page.handle_key(key("Up"))
	page.handle_key(key("Enter"))
	assert page.handle_key(key("q")).action == PageAction.POP


def test_branch_picker_checkout_branch(committed_repo: Repository) -> None:
	commit = committed_repo.get(committed_repo.head.target)
	committed_repo.branches.create("feature", commit)
	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = branch_picker_page()
	page.on_enter(ctx)
	page.handle_key(key("Down"))
	page.handle_key(key("Enter"))
	assert page.handle_key(key("Enter")).action == PageAction.POP


def test_branch_picker_create_branch(committed_repo: Repository) -> None:
	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = branch_picker_page()
	page.on_enter(ctx)
	page.handle_key(key("n"))
	page.handle_key(key("e"))
	page.handle_key(key("w"))
	page.handle_key(key("Enter"))
	assert "new" in committed_repo.branches.local


def test_branch_picker_create_invalid_shows_error(committed_repo: Repository) -> None:
	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = branch_picker_page()
	page.on_enter(ctx)
	page.handle_key(key(" "))
	page.handle_key(key("Enter"))
	assert page.status_text()


def test_branch_picker_draw_zero_height() -> None:
	page = BranchPickerPage()
	page.draw(MagicMock(), Rect(0, 0, 80, 0))


def test_projects_page_lists_repos(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
	root = tmp_path / "root"
	project = root / "demo"
	project.mkdir(parents=True)
	from pygit2 import init_repository

	repo = init_repository(str(project), bare=False)
	workdir = Path(repo.workdir)
	(workdir / "README").write_text("x\n", encoding="utf-8")
	repo.index.add("README")
	repo.index.write()
	tree = repo.index.write_tree()
	sig = Signature("t", "t@example.com")
	repo.create_commit("HEAD", sig, sig, "init", tree, [])
	settings = tmp_path / "settings.json"
	settings.write_text(json.dumps({"PROJECTROOT": str(root)}), encoding="utf-8")
	monkeypatch.setenv("PYGITWEB_SETTINGS_CONFIG", str(settings))
	ctx = PageContext(repo=Repository(str(project / ".git")), repo_path=project / ".git")
	page = projects_page()
	page.on_enter(ctx)
	page.draw(MagicMock(), Rect(0, 0, 80, 10))
	page.handle_key(key("Down"))
	result = page.handle_key(key("Enter"))
	assert result.switch_repo == project.resolve()


def test_projects_page_empty(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
	settings = tmp_path / "settings.json"
	settings.write_text(json.dumps({"PROJECTROOT": str(tmp_path / "empty")}), encoding="utf-8")
	monkeypatch.setenv("PYGITWEB_SETTINGS_CONFIG", str(settings))
	page = projects_page()
	page.on_enter(PageContext(repo=MagicMock(), repo_path=tmp_path))  # type: ignore[arg-type]
	assert "No projects" in page.status_text()
	page.handle_key(key("Enter"))


def test_projects_page_draw_zero_height() -> None:
	ProjectsPage().draw(MagicMock(), Rect(0, 0, 80, 0))


def _empty_tree() -> Oid:
	return Oid(hex=EMPTY_TREE_OID_HEX)


def test_home_page_tasks_tab_creates_and_edits_tasks(committed_repo: Repository) -> None:
	board_ref = f"{BOARD_REF_PREFIX}Tasks"
	board = Board(_empty_tree(), board_ref, tagger="", description="Main")
	task = Task(
		_empty_tree(),
		f"{TASK_REF_PREFIX}alpha",
		"alice <alice@example.com>",
		title="Alpha",
		description="Do alpha",
		status=Task.Status.TODO,
	)
	task_oid = task.write(committed_repo)
	board.tasks = [str(task_oid)]
	board.update_message()
	board.write(committed_repo)

	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = home_page()
	page.on_enter(ctx)
	page.set_active_tab("tasks")
	page.draw(MagicMock(), Rect(0, 0, 80, 20))

	# Move to first status header and then to (+ New Task)
	page.handle_key(key("Down"))
	page.handle_key(key("Down"))
	result_new = page.handle_key(key("Enter"))
	assert result_new.action == PageAction.PUSH
	assert isinstance(result_new.next_page, TaskEditorPage)

	# Move to existing task row and open editor
	page = home_page()
	page.on_enter(ctx)
	page.set_active_tab("tasks")
	page.handle_key(key("Down"))
	page.handle_key(key("Down"))
	page.handle_key(key("Down"))
	result_existing = page.handle_key(key("Enter"))
	assert result_existing.action == PageAction.PUSH
	assert isinstance(result_existing.next_page, TaskEditorPage)