from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from pygit2 import Repository, init_repository
from pyratatui import Rect

from pygittools.tui.app import _apply_result, run_tui
from pygittools.tui.changes_list import ChangesList, SectionId
from pygittools.tui.commit_draft import commit_editmsg_path, load_commit_draft
from pygittools.tui.input_context import InputContext, nav_context
from pygittools.tui.input_context_test import FakeHost
from pygittools.tui.keys import key
from pygittools.tui.log_list import CommitLogList
from pygittools.tui.pages.branches import BranchPickerPage
from pygittools.tui.pages.help import HelpPage
from pygittools.tui.pages.home import home_page
from pygittools.tui.pages.no_repo import NoRepoPage
from pygittools.tui.pages.projects import ProjectsPage
from pygittools.tui.projects import ProjectEntry
from pygittools.tui.pygitweb_layout import _bool_value, _int_value, _layout_from_mapping
from pygittools.tui.status_bar import _format_line
from pygittools.tui.tasks_list import TasksBoardList
from pygittools.tui.types import PageAction, PageContext, PageResult
from pygittools.tui.worktree import commit_staged, unstage_paths
from pygittools.tui.worktrees_list import WorktreesList


def test_read_draft_skips_blank_lines(tmp_path: Path) -> None:
	commit_editmsg_path(tmp_path).write_text("\n\n# comment\n", encoding="utf-8")
	assert load_commit_draft(tmp_path) == ""


def test_status_bar_tight_left_space() -> None:
	line = _format_line("left", "hint", 5)
	assert line == "hint "


def test_layout_from_mapping_empty_projects_list() -> None:
	layout = _layout_from_mapping({"PROJECTROOT": "/tmp", "PROJECTS_LIST": ""})
	assert layout.projects_list == Path("/tmp")


def test_int_and_bool_defaults() -> None:
	assert _int_value(object(), 9) == 9
	assert _bool_value(object(), False) is False


def test_changes_list_working_tree_clean_hint() -> None:
	changes = ChangesList()
	assert changes.status_hint() == "Working tree clean"


def test_changes_list_input_unknown_key(committed_repo: Repository) -> None:
	changes = ChangesList()
	changes.refresh(committed_repo)
	assert changes.handle_input_key(key("Null"), committed_repo).layout_changed is False


def test_changes_list_nav_with_no_rows_after_collapse(committed_repo: Repository) -> None:
	changes = ChangesList()
	changes.refresh(committed_repo)
	changes._rows = []
	assert changes.handle_nav_key(key("Up"), committed_repo).layout_changed is False


def test_changes_list_commit_hints_and_edit(committed_repo: Repository) -> None:
	changes = ChangesList()
	changes.set_commit_message("msg")
	changes.refresh(committed_repo)
	changes._list_state.select(0)
	assert "Enter edit" in changes.status_hint()
	assert changes.handle_nav_key(key("Enter"), committed_repo).enter_input is True
	changes.handle_input_key(key("Backspace"), committed_repo)
	assert changes.commit_message == "ms"


def test_changes_list_staged_header_unstage_all(committed_repo: Repository) -> None:
	path = Path(committed_repo.workdir) / "new.txt"
	path.write_text("x\n", encoding="utf-8")
	committed_repo.index.add("new.txt")
	committed_repo.index.write()
	changes = ChangesList()
	changes.refresh(committed_repo)
	for index, row in enumerate(changes._rows):
		if row.kind == "header" and row.section == SectionId.STAGED:
			changes._list_state.select(index)
			break
	changes.handle_nav_key(key("Enter"), committed_repo)


def test_changes_list_unstage_file_row(committed_repo: Repository) -> None:
	path = Path(committed_repo.workdir) / "new.txt"
	path.write_text("x\n", encoding="utf-8")
	committed_repo.index.add("new.txt")
	committed_repo.index.write()
	changes = ChangesList()
	changes.refresh(committed_repo)
	for index, row in enumerate(changes._rows):
		if row.kind == "file" and row.section == SectionId.STAGED:
			changes._list_state.select(index)
			changes.handle_nav_key(key("Enter"), committed_repo)
			break


def test_changes_list_header_right_key(committed_repo: Repository) -> None:
	changes = ChangesList()
	changes.refresh(committed_repo)
	for index, row in enumerate(changes._rows):
		if row.kind == "header":
			changes._list_state.select(index)
			changes.handle_nav_key(key("Right"), committed_repo)
			break


def test_changes_list_failed_commit_nav(committed_repo: Repository) -> None:
	changes = ChangesList()
	changes.set_commit_message("msg")
	changes.refresh(committed_repo)
	changes._list_state.select(0)
	changes.handle_nav_key(key("Enter"), committed_repo)
	assert changes.status_hint()


def test_log_list_scroll_and_refresh(committed_repo: Repository) -> None:
	log = CommitLogList()
	log.refresh(committed_repo)
	log.draw(MagicMock(), Rect(0, 0, 80, 0), highlight=True)
	log._list_state.select(100)
	log.draw(MagicMock(), Rect(0, 0, 80, 2), highlight=True)
	log.handle_nav_key(key("Up"), committed_repo)


def test_input_context_log_tab_at_top(page_ctx: PageContext) -> None:
	host = FakeHost(
		page_context=page_ctx,
		changes=ChangesList(),
		log=CommitLogList(),
		tasks=TasksBoardList(),
		worktrees=WorktreesList(),
	)
	host.active_tab = "log"
	host.log._list_state.select(0)
	nav_context().dispatch_key(host, key("Up"))
	assert host.header_focus == "branch"


def test_input_context_input_without_relinquish(page_ctx: PageContext) -> None:
	host = FakeHost(
		page_context=page_ctx,
		changes=ChangesList(),
		log=CommitLogList(),
		tasks=TasksBoardList(),
		worktrees=WorktreesList(),
	)
	dispatch = InputContext().dispatch_key(host, key("x"))
	assert dispatch.next_mode is None


def test_branch_picker_error_and_short_list(committed_repo: Repository) -> None:
	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = BranchPickerPage()
	page.on_enter(ctx)
	page._error = "bad"
	assert "bad" in page.status_text()
	page.draw(MagicMock(), Rect(0, 0, 80, 2))
	page._ctx = None
	page.draw(MagicMock(), Rect(0, 0, 80, 10))
	page.handle_key(key("Enter"))


def test_branch_picker_checkout_error(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = BranchPickerPage()
	page.on_enter(ctx)
	page._filtered = ["ghost"]
	page._focus_filter = False
	page._list_state.select(0)
	monkeypatch.setattr(
		"pygittools.tui.pages.branches.checkout_branch",
		lambda _repo, _branch: (False, "nope"),
	)
	page.handle_key(key("Enter"))
	assert page.status_text() == "nope"


def test_help_page_empty_lines(monkeypatch: pytest.MonkeyPatch) -> None:
	page = HelpPage()
	monkeypatch.setattr(page, "_lines", [])
	assert page.status_text() == "Help"
	page.draw(MagicMock(), Rect(0, 0, 80, 0))


def test_help_page_scroll_past_end() -> None:
	page = HelpPage()
	page._state.select(999)
	page.draw(MagicMock(), Rect(0, 0, 80, 3))
	page.handle_key(key("Null"))


def test_home_page_relinquish_input(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
	ctx = PageContext(repo=committed_repo, repo_path=Path(committed_repo.path))
	page = home_page()
	page.on_enter(ctx)
	page.handle_key(key("/"))
	for char in "commit":
		page.handle_key(key(char))
	page.handle_key(key("Enter"))
	page.handle_key(key("a"))
	monkeypatch.setattr(
		"pygittools.tui.changes_list.commit_staged",
		lambda _repo, _msg, *, amend=False: (True, ""),
	)
	page.handle_key(key("Tab"))
	page.handle_key(key("Enter"))
	assert page.status_text()


def test_no_repo_page_truncates_lines(tmp_path: Path) -> None:
	page = NoRepoPage(tmp_path)
	page.draw(MagicMock(), Rect(0, 0, 80, 1))
	page.handle_key(key("Null"))


def test_projects_page_error_and_scroll(tmp_path: Path) -> None:
	page = ProjectsPage()
	page._projects = [
		ProjectEntry(path="x" * 100, worktree=tmp_path, branch="b" * 30),
	]
	page._projectroot_label = "root"
	page._error = "bad"
	assert page.status_text() == "bad"
	page.draw(MagicMock(), Rect(0, 0, 40, 5))
	page.handle_key(key("Up"))
	page.handle_key(key("q"))


def test_unstage_paths_empty(committed_repo: Repository) -> None:
	unstage_paths(committed_repo, [])


def test_commit_staged_no_workdir(tmp_path: Path) -> None:
	bare = init_repository(str(tmp_path / "bare"), bare=True)
	ok, error = commit_staged(bare, "msg")
	assert ok is False
	assert error == "Repository has no working directory"


def test_apply_result_switch_repo_missing(tmp_path: Path, page_ctx: PageContext) -> None:
	home = home_page()
	home.on_enter(page_ctx)
	ctx, stack = _apply_result(
		[home],
		page_ctx,
		PageResult(action=PageAction.POP, switch_repo=tmp_path / "missing"),
	)
	assert ctx is page_ctx


class _FakeTerm:
	def __init__(self, keys: list[object]) -> None:
		self._keys = list(keys)
		self.hide_cursor = MagicMock()

	def __enter__(self) -> _FakeTerm:
		return self

	def __exit__(self, *args: object) -> bool:
		return False

	def draw(self, draw_fn: object) -> None:
		frame = MagicMock()
		frame.area = Rect(0, 0, 80, 24)
		draw_fn(frame)  # type: ignore[operator]

	def poll_event(self, timeout_ms: int = 0) -> object | None:
		del timeout_ms
		if not self._keys:
			return key("q")
		return self._keys.pop(0)


def test_run_tui_keyboard_interrupt(committed_repo: Repository) -> None:
	repo_dir = Path(committed_repo.workdir)

	class BoomTerm(_FakeTerm):
		def poll_event(self, timeout_ms: int = 0) -> object | None:
			del timeout_ms
			raise KeyboardInterrupt

	with (
		patch("pygittools.tui.app.Terminal", return_value=BoomTerm([])),
		patch("pygittools.tui.app.init_tui_color_theme"),
		patch("pygittools.tui.app.init_tab_colors"),
	):
		assert run_tui(repo_dir) == 0


def test_run_tui_idle_then_quit(committed_repo: Repository) -> None:
	repo_dir = Path(committed_repo.workdir)
	with (
		patch("pygittools.tui.app.Terminal", return_value=_FakeTerm([None, key("q")])),
		patch("pygittools.tui.app.init_tui_color_theme"),
		patch("pygittools.tui.app.init_tab_colors"),
	):
		assert run_tui(repo_dir) == 0