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