from __future__ import annotations

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

import pytest
from pygit2 import Repository, init_repository
from unicurses import KEY_BACKSPACE, KEY_ENTER, KEY_RESIZE, KEY_RIGHT, KEY_UP

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.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.tabs import _clip_prefix, tab_row_layout
from pygittools.tui.types import PageAction, PageContext, PageResult
from pygittools.tui.worktree import commit_staged, unstage_paths


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_draw_text_zero_clip_width(monkeypatch: pytest.MonkeyPatch) -> None:
	from pygittools.tui import draw

	monkeypatch.setattr(draw, "move", MagicMock())
	monkeypatch.setattr(draw, "mvaddstr", MagicMock())
	assert draw.draw_text(0, 9, "x", 10) == 9


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


def test_tab_row_layout_multiple_tabs() -> None:
	layout = tab_row_layout("p", ("A", "B", "C"))
	assert len(layout) == 3


def test_clip_prefix_fits_without_ellipsis() -> None:
	assert _clip_prefix("x", 80, ("Status", "Log")) == "x"


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_resize_and_unknown_key(committed_repo: Repository) -> None:
	changes = ChangesList()
	changes.refresh(committed_repo)
	assert changes.handle_input_key(KEY_RESIZE, committed_repo).layout_changed is False
	assert changes.handle_input_key(999, 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_backspace(committed_repo: Repository) -> None:
	changes = ChangesList()
	changes.set_commit_message("msg")
	changes.refresh(committed_repo)
	changes._cursor = 0
	assert "Enter commit" in changes.status_hint()
	changes.handle_nav_key(KEY_BACKSPACE, committed_repo)
	changes.handle_nav_key(KEY_BACKSPACE, committed_repo)


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._cursor = 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._cursor = 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._cursor = 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._cursor = 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(0, 0, 80, highlight=True)
	log._cursor = 100
	log.draw(0, 2, 80, 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())
	host.active_tab = "log"
	host.log._cursor = 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())
	dispatch = InputContext().dispatch_key(host, ord("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(0, 2, 80)
	page._ctx = None
	page.draw(0, 10, 80)
	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._cursor = 1
	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(0, 0, 80)


def test_help_page_scroll_past_end() -> None:
	page = HelpPage()
	page._offset = 999
	page.draw(0, 3, 80)
	page.handle_key(999)


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(ord("a"))
	monkeypatch.setattr(
		"pygittools.tui.changes_list.commit_staged",
		lambda _repo, _msg: (True, ""),
	)
	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(0, 1, 80)
	page.handle_key(999)


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(0, 5, 40)
	page.handle_key(KEY_UP)
	page.handle_key(ord("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


def test_run_tui_keyboard_interrupt(committed_repo: Repository) -> None:
	repo_dir = Path(committed_repo.workdir)
	with patch("pygittools.tui.app.initscr") as initscr:
		initscr.return_value = MagicMock()
		with (
			patch("pygittools.tui.app.getmaxyx", return_value=(24, 80)),
			patch("pygittools.tui.app.getch", side_effect=KeyboardInterrupt),
			patch("pygittools.tui.app.noecho"),
			patch("pygittools.tui.app.cbreak"),
			patch("pygittools.tui.app.curs_set"),
			patch("pygittools.tui.app.keypad"),
			patch("pygittools.tui.app.clear"),
			patch("pygittools.tui.app.refresh"),
			patch("pygittools.tui.app.endwin"),
			patch("pygittools.tui.app.init_status_bar", return_value=0),
			patch("pygittools.tui.app.init_tab_colors"),
			patch("pygittools.tui.app.draw_status_bar"),
		):
			assert run_tui(repo_dir) == 0


def test_run_tui_resize_key(committed_repo: Repository) -> None:
	repo_dir = Path(committed_repo.workdir)
	with patch("pygittools.tui.app.initscr") as initscr:
		initscr.return_value = MagicMock()
		with (
			patch("pygittools.tui.app.getmaxyx", return_value=(24, 80)),
			patch("pygittools.tui.app.getch", side_effect=[KEY_RESIZE, ord("q")]),
			patch("pygittools.tui.app.noecho"),
			patch("pygittools.tui.app.cbreak"),
			patch("pygittools.tui.app.curs_set"),
			patch("pygittools.tui.app.keypad"),
			patch("pygittools.tui.app.clear"),
			patch("pygittools.tui.app.refresh"),
			patch("pygittools.tui.app.endwin"),
			patch("pygittools.tui.app.init_status_bar", return_value=0),
			patch("pygittools.tui.app.init_tab_colors"),
			patch("pygittools.tui.app.draw_status_bar"),
		):
			assert run_tui(repo_dir) == 0