from __future__ import annotations

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

import pytest
from pygit2 import Repository
from pyratatui import Rect

from pygittools.tui.changes_list import ChangesList, SectionId
from pygittools.tui.keys import key, set_vim_enabled
from pygittools.tui.types import PageContext
from pygittools.tui.worktree import categorize_status


def test_refresh_builds_sections(committed_repo: Repository) -> None:
    changes = ChangesList()
    changes.refresh(committed_repo)
    assert changes.status_hint() != ""


def test_refresh_clean_repo(page_ctx: PageContext) -> None:
    changes = ChangesList()
    changes.refresh(page_ctx.repo)
    assert "Enter edit" in changes.status_hint()


def test_nav_up_down(page_ctx: PageContext) -> None:
    changes = ChangesList()
    changes.refresh(page_ctx.repo)
    changes.handle_nav_key(key("Down"), page_ctx.repo)
    changes.handle_nav_key(key("Up"), page_ctx.repo)
    assert changes.is_at_top()


def test_stage_and_unstage_file(committed_repo: Repository) -> None:
    path = Path(committed_repo.workdir) / "tracked.txt"
    path.write_text("v2\n", encoding="utf-8")
    changes = ChangesList()
    changes.refresh(committed_repo)
    changes._list_state.select(3)
    changes.handle_nav_key(key("Enter"), committed_repo)
    changes.refresh(committed_repo)
    staged, unstaged, _ = categorize_status(committed_repo)
    assert "tracked.txt" in staged or "tracked.txt" in unstaged


def test_toggle_section_stages_untracked(committed_repo: Repository) -> None:
    (Path(committed_repo.workdir) / "new.txt").write_text("x\n", encoding="utf-8")
    changes = ChangesList()
    changes.refresh(committed_repo)
    for index, row in enumerate(changes._rows):
        if row.kind == "header" and row.section == SectionId.UNTRACKED:
            changes._list_state.select(index)
            break
    result = changes.handle_nav_key(key("Enter"), committed_repo)
    assert result.layout_changed is True


def test_collapse_section_with_space(committed_repo: Repository) -> None:
    (Path(committed_repo.workdir) / "new.txt").write_text("x\n", encoding="utf-8")
    changes = ChangesList()
    changes.refresh(committed_repo)
    for index, row in enumerate(changes._rows):
        if row.kind == "header" and row.section == SectionId.UNSTAGED:
            changes._list_state.select(index)
            break
    result = changes.handle_nav_key(key(" "), committed_repo)
    assert result.layout_changed is True


def test_collapse_does_not_reread_status(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)
            break
    with patch("pygittools.tui.changes_list.categorize_status") as status:
        changes.handle_nav_key(key(" "), committed_repo)
    status.assert_not_called()
    assert any(row.kind == "header" and row.label.startswith("[>") for row in changes._rows)


def test_commit_input_flow(committed_repo: Repository) -> None:
    (Path(committed_repo.workdir) / "new.txt").write_text("x\n", encoding="utf-8")
    committed_repo.index.add("new.txt")
    committed_repo.index.write()
    changes = ChangesList()
    changes.refresh(committed_repo)
    changes._list_state.select(0)
    assert changes.handle_nav_key(key("Enter"), committed_repo).enter_input is True
    changes.handle_input_key(key("f"), committed_repo)
    assert changes.commit_message.endswith("f")
    changes.handle_input_key(key("Enter"), committed_repo)
    assert "\n" in changes.commit_message
    changes.handle_input_key(key("Tab"), committed_repo)
    result = changes.handle_input_key(key("Enter"), committed_repo)
    assert result.committed is True
    assert result.relinquish is True


def test_commit_button_with_message(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
    changes = ChangesList()
    changes.refresh(committed_repo)
    changes.set_commit_message("msg")
    changes.begin_input()
    monkeypatch.setattr(
        "pygittools.tui.changes_list.commit_staged",
        lambda _repo, _msg, *, amend=False: (True, ""),
    )
    changes.handle_input_key(key("Tab"), committed_repo)
    result = changes.handle_input_key(key("Enter"), committed_repo)
    assert result.layout_changed is True
    assert result.committed is True


def test_amend_button(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
    changes = ChangesList()
    changes.refresh(committed_repo)
    changes.set_commit_message("fix")
    changes.begin_input()
    seen: dict[str, bool] = {}

    def _commit(_repo: Repository, _msg: str, *, amend: bool = False) -> tuple[bool, str]:
        seen["amend"] = amend
        return True, ""

    monkeypatch.setattr("pygittools.tui.changes_list.commit_staged", _commit)
    changes.handle_input_key(key("Tab"), committed_repo)
    changes.handle_input_key(key("Right"), committed_repo)
    result = changes.handle_input_key(key("Enter"), committed_repo)
    assert result.committed is True
    assert seen["amend"] is True


def test_commit_button_failure(committed_repo: Repository, monkeypatch: pytest.MonkeyPatch) -> None:
    changes = ChangesList()
    changes.refresh(committed_repo)
    changes.set_commit_message("msg")
    changes.begin_input()
    monkeypatch.setattr(
        "pygittools.tui.changes_list.commit_staged",
        lambda _repo, _msg, *, amend=False: (False, "nope"),
    )
    changes.handle_input_key(key("Tab"), committed_repo)
    changes.handle_input_key(key("Enter"), committed_repo)
    assert "nope" in changes.input_status_hint()


def test_enter_commit_row_starts_input(committed_repo: Repository) -> None:
    changes = ChangesList()
    changes.refresh(committed_repo)
    changes._list_state.select(0)
    result = changes.handle_nav_key(key("Enter"), committed_repo)
    assert result.enter_input is True


def test_vim_i_starts_input(committed_repo: Repository) -> None:
    set_vim_enabled(True)
    try:
        changes = ChangesList()
        changes.refresh(committed_repo)
        changes._list_state.select(0)
        assert "Enter/i" in changes.status_hint()
        result = changes.handle_nav_key(key("i"), committed_repo)
        assert result.enter_input is True
    finally:
        set_vim_enabled(False)


def test_stage_file_hint(committed_repo: Repository) -> None:
    path = Path(committed_repo.workdir) / "tracked.txt"
    path.write_text("v2\n", encoding="utf-8")
    changes = ChangesList()
    changes.refresh(committed_repo)
    for index, row in enumerate(changes._rows):
        if row.kind == "file" and row.section == SectionId.UNSTAGED:
            changes._list_state.select(index)
            break
    assert "Enter stage" in changes.status_hint()


def test_draw_renders(committed_repo: Repository) -> None:
    changes = ChangesList()
    changes.refresh(committed_repo)
    frame = MagicMock()
    area = Rect(0, 0, 80, 20)
    changes.draw(frame, area, highlight=True, commit_input=True)
    changes.draw(frame, area, highlight=True, commit_input=False)


def test_draw_commit_input_does_not_highlight_shifted_row(committed_repo: Repository) -> None:
    changes = ChangesList()
    changes.refresh(committed_repo)
    changes._list_state.select(0)
    frame = MagicMock()
    changes.draw(frame, Rect(0, 0, 80, 20), highlight=True, commit_input=True)
    assert frame.render_stateful_list.called
    list_state = frame.render_stateful_list.call_args.args[2]
    assert list_state.selected is None
    assert (changes._list_state.selected or 0) == 0


def test_persist_commit_draft(page_ctx: PageContext) -> None:
    changes = ChangesList()
    changes.set_commit_message("draft")
    changes.persist_commit_draft(page_ctx.repo)


def test_toggle_staged_header_when_empty(committed_repo: Repository) -> None:
    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
    result = changes.handle_nav_key(key("Enter"), committed_repo)
    assert result.layout_changed is True