from pathlib import Path

import pytest

from pulka.api.runtime import Runtime
from pulka.testing.data import make_df, write_df
from pulka.tui.keymap import KEY_ONLY_ACTION_SPECS, _quit_back_with_confirmation
from pulka.tui.screen import Screen


@pytest.fixture
def sample_dataset(tmp_path: Path) -> str:
    df = make_df("mini_nav", rows=8, cols=3, seed=123)
    path = tmp_path / "runtime.parquet"
    write_df(df, path, "parquet")
    return str(path)


def _session_without_entrypoints(path: str):
    runtime = Runtime(load_entry_points=False)
    return runtime.open(path, viewport_rows=6)


def test_key_only_help_entries_match_spec(sample_dataset: str) -> None:
    session = _session_without_entrypoints(sample_dataset)
    screen = Screen(session.viewer)
    try:
        help_entries = screen.commands_help_entries()
        expected = {
            (spec.help_keys, spec.help_action, spec.help_description)
            for spec in KEY_ONLY_ACTION_SPECS
            if spec.add_help
        }
        expected.add(("rt<id>", "Remove transform", "Remove transform by id"))
        actual = {(entry.aliases, entry.command, entry.description) for entry in help_entries}
        assert actual == expected
    finally:
        unsubscribe = getattr(screen, "_view_stack_unsubscribe", None)
        if unsubscribe is not None:
            unsubscribe()
        session.close()


class _StubApp:
    def __init__(self) -> None:
        self.exit_called = False

    def exit(self) -> None:
        self.exit_called = True


class _StubEvent:
    def __init__(self, app: _StubApp) -> None:
        self.app = app


def test_q_confirms_before_exit_at_root(sample_dataset: str) -> None:
    session = _session_without_entrypoints(sample_dataset)
    screen = Screen(session.viewer)
    app = _StubApp()
    event = _StubEvent(app)
    recorded_modal: dict[str, object] = {}
    refreshed = False
    try:
        screen._record_key_event = lambda _event: None  # type: ignore[method-assign]

        def _capture_modal(**kwargs) -> None:  # type: ignore[no-untyped-def]
            recorded_modal.update(kwargs)

        def _refresh() -> None:
            nonlocal refreshed
            refreshed = True

        screen._open_confirmation_modal = _capture_modal  # type: ignore[method-assign]
        screen.refresh = _refresh  # type: ignore[method-assign]

        _quit_back_with_confirmation(screen, event)

        assert not app.exit_called
        assert recorded_modal.get("title") == "Exit pulka?"
        assert refreshed

        on_confirm = recorded_modal.get("on_confirm")
        assert callable(on_confirm)
        on_confirm()
        assert app.exit_called
    finally:
        unsubscribe = getattr(screen, "_view_stack_unsubscribe", None)
        if unsubscribe is not None:
            unsubscribe()
        session.close()


def test_q_pops_viewer_when_derived(sample_dataset: str) -> None:
    session = _session_without_entrypoints(sample_dataset)
    screen = Screen(session.viewer)
    app = _StubApp()
    event = _StubEvent(app)
    pop_called = False
    modal_called = False
    refreshed = False
    try:
        screen._record_key_event = lambda _event: None  # type: ignore[method-assign]

        class _DerivedStack:
            def __len__(self) -> int:
                return 2

        def _pop() -> None:
            nonlocal pop_called
            pop_called = True

        def _modal(**_kwargs) -> None:  # type: ignore[no-untyped-def]
            nonlocal modal_called
            modal_called = True

        def _refresh() -> None:
            nonlocal refreshed
            refreshed = True

        screen.view_stack = _DerivedStack()  # type: ignore[assignment]
        screen._pop_viewer = _pop  # type: ignore[method-assign]
        screen._open_confirmation_modal = _modal  # type: ignore[method-assign]
        screen.refresh = _refresh  # type: ignore[method-assign]

        _quit_back_with_confirmation(screen, event)

        assert pop_called
        assert refreshed
        assert not modal_called
        assert not app.exit_called
    finally:
        unsubscribe = getattr(screen, "_view_stack_unsubscribe", None)
        if unsubscribe is not None:
            unsubscribe()
        session.close()
