"""C5 widget tests: chips, tiles, empty states, delegates (offscreen)."""

from __future__ import annotations

import pytest

pytest.importorskip("PySide6")
pytest.importorskip("pytestqt")

from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt

from mgf.brand_qt.tokens import STATE_HUES
from mgf.brand_qt.widgets import (
    STATUS_ROLE,
    Badge,
    EmptyState,
    StatTile,
    StatusChip,
    StatusChipDelegate,
    bind_empty_state,
)

pytestmark = pytest.mark.gui


class TestStatusChip:
    def test_every_state_renders(self, qtbot) -> None:
        chip = StatusChip()
        qtbot.addWidget(chip)
        for state in STATE_HUES:
            chip.set_state(state, state)
            assert chip.state == state
            assert chip.text() == state
            assert chip.property("mgfState") == state

    def test_unknown_state_raises(self, qtbot) -> None:
        chip = StatusChip()
        qtbot.addWidget(chip)
        with pytest.raises(ValueError, match="unknown chip state"):
            chip.set_state("nonsense")

    def test_busy_pulses_only_when_animation_enabled(self, qtbot) -> None:
        animated = StatusChip("busy", "working", animate=True)
        qtbot.addWidget(animated)
        animated.show()
        assert animated._pulse is not None
        still = StatusChip("busy", "working", animate=False)
        qtbot.addWidget(still)
        still.show()
        assert still._pulse is None
        animated.set_state("success")
        assert animated._pulse is None

    def test_busy_pulse_requires_visibility(self, qtbot) -> None:
        """PAPER-02: a hidden chip must not keep an infinite animation alive."""
        chip = StatusChip("busy", "working", animate=True)
        qtbot.addWidget(chip)
        # Seeded defect: before the fix the pulse ran on a never-shown chip.
        assert chip._pulse is None
        chip.show()
        assert chip._pulse is not None
        chip.hide()
        assert chip._pulse is None
        chip.show()
        assert chip._pulse is not None  # false-positive pin: show resumes

    def test_set_animate_toggles_a_live_pulse(self, qtbot) -> None:
        """PAPER-02 follow-up: _animate is no longer constructor-frozen."""
        chip = StatusChip("busy", "working", animate=True)
        qtbot.addWidget(chip)
        chip.show()
        assert chip._pulse is not None
        chip.set_animate(False)
        assert chip._pulse is None
        chip.set_animate(True)
        assert chip._pulse is not None

    def test_reduce_motion_policy_stops_the_pulse(self, qtbot) -> None:
        """The kit policy gates the one looping animation it ships."""
        from mgf.brand_qt.motion import set_reduce_motion

        chip = StatusChip("busy", "working", animate=True)
        qtbot.addWidget(chip)
        chip.show()
        assert chip._pulse is not None
        try:
            set_reduce_motion(True)
            chip.set_animate(True)  # any resync consults the policy
            assert chip._pulse is None
        finally:
            set_reduce_motion(False)

    def test_badge_never_animates(self, qtbot) -> None:
        badge = Badge("busy", "queued")
        qtbot.addWidget(badge)
        badge.show()
        assert badge._pulse is None


class TestStatTile:
    def test_value_roundtrip_and_accent_property(self, qtbot) -> None:
        tile = StatTile("Running", "3", accent=True)
        qtbot.addWidget(tile)
        assert tile.value() == "3"
        tile.set_value("4")
        assert tile.value() == "4"
        assert tile.property("mgfAccent") == "true"
        assert tile.property("mgfClass") == "statTile"


_ROOT = QModelIndex()


class _OneColumnModel(QAbstractTableModel):
    def __init__(self) -> None:
        super().__init__()
        self.rows: list[tuple[str, str]] = []

    def rowCount(self, parent=_ROOT) -> int:
        return len(self.rows)

    def columnCount(self, parent=_ROOT) -> int:
        return 1

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if not index.isValid():
            return None
        text, state = self.rows[index.row()]
        if role == Qt.ItemDataRole.DisplayRole:
            return text
        if role == STATUS_ROLE:
            return state
        return None

    def set_rows(self, rows: list[tuple[str, str]]) -> None:
        self.beginResetModel()
        self.rows = rows
        self.endResetModel()


class TestEmptyStateBinding:
    def test_swaps_visibility_with_model_content(self, qtbot) -> None:
        from PySide6.QtWidgets import QTableView, QVBoxLayout, QWidget

        host = QWidget()
        layout = QVBoxLayout(host)
        view = QTableView()
        model = _OneColumnModel()
        view.setModel(model)
        empty = EmptyState("kernel", "Nothing here", "Add something.")
        layout.addWidget(view)
        layout.addWidget(empty)
        qtbot.addWidget(host)
        host.show()
        bind_empty_state(view, empty)
        assert not view.isVisible() and empty.isVisible()
        model.set_rows([("row", "success")])
        assert view.isVisible() and not empty.isVisible()
        model.set_rows([])
        assert not view.isVisible() and empty.isVisible()

    def test_action_button_fires_callback(self, qtbot) -> None:
        fired: list[bool] = []
        empty = EmptyState(
            "kernel", "t", "r", action_text="Do it", on_action=lambda: fired.append(True)
        )
        qtbot.addWidget(empty)
        assert empty.action_button is not None
        empty.action_button.click()
        assert fired == [True]

    def test_copy_mutates_in_place(self, qtbot) -> None:
        """PAPER-08: reason changes must not cost a rebuild."""
        empty = EmptyState("kernel", "No devices", "The broker is empty.")
        qtbot.addWidget(empty)
        empty.set_title("Broker declined")
        empty.set_reason("This operator lacks the claim capability.")
        assert empty.title_label.text() == "Broker declined"
        assert "claim capability" in empty.reason_label.text()
        empty.set_icon("lock")  # validates against the 42-icon set
        with pytest.raises(ValueError, match="unknown brand icon"):
            empty.set_icon("nope")

    def test_unbind_releases_every_connection(self, qtbot) -> None:
        """PAPER-01 seeded defect: without unbind, a dead pane keeps toggling."""
        from PySide6.QtWidgets import QTableView, QVBoxLayout, QWidget

        host = QWidget()
        layout = QVBoxLayout(host)
        view = QTableView()
        model = _OneColumnModel()
        view.setModel(model)
        empty = EmptyState("kernel", "t", "r")
        layout.addWidget(view)
        layout.addWidget(empty)
        qtbot.addWidget(host)
        host.show()
        binding = bind_empty_state(view, empty)
        assert not view.isVisible()
        binding.unbind()
        model.set_rows([("row", "success")])
        # After unbind the binding must be inert: visibility unchanged.
        assert not view.isVisible() and empty.isVisible()
        binding.unbind()  # idempotent — second call must not raise

    def test_data_changed_resyncs(self, qtbot) -> None:
        """PAPER-01: dataChanged is part of the watched signal set."""
        from PySide6.QtWidgets import QTableView, QVBoxLayout, QWidget

        host = QWidget()
        layout = QVBoxLayout(host)
        view = QTableView()
        model = _OneColumnModel()
        model.set_rows([("row", "success")])
        view.setModel(model)
        empty = EmptyState("kernel", "t", "r")
        layout.addWidget(view)
        layout.addWidget(empty)
        qtbot.addWidget(host)
        host.show()
        bind_empty_state(view, empty)
        assert view.isVisible()
        # Force a hide, then emit dataChanged: the binding must resync.
        view.hide()
        empty.show()
        model.dataChanged.emit(model.index(0, 0), model.index(0, 0))
        assert view.isVisible() and not empty.isVisible()

    def test_sync_reads_the_live_model_not_the_bound_one(self, qtbot) -> None:
        """PAPER-01: the closure bug tracked the bind-time model forever."""
        from PySide6.QtWidgets import QTableView, QVBoxLayout, QWidget

        host = QWidget()
        layout = QVBoxLayout(host)
        view = QTableView()
        first = _OneColumnModel()
        view.setModel(first)
        empty = EmptyState("kernel", "t", "r")
        layout.addWidget(view)
        layout.addWidget(empty)
        qtbot.addWidget(host)
        host.show()
        bind_empty_state(view, empty)
        second = _OneColumnModel()
        second.set_rows([("row", "success")])
        view.setModel(second)
        # A signal from the old (still-connected) model must judge
        # emptiness by the view's CURRENT model, which has rows.
        first.set_rows([])
        assert view.isVisible() and not empty.isVisible()


class TestStatusChipDelegate:
    def test_renders_chip_cells_without_error(self, qtbot) -> None:
        from PySide6.QtWidgets import QTableView

        view = QTableView()
        model = _OneColumnModel()
        model.set_rows([("running", "success"), ("weird", "not-a-state"), ("plain", None)])
        view.setModel(model)
        view.setItemDelegateForColumn(0, StatusChipDelegate(view))
        qtbot.addWidget(view)
        view.resize(300, 200)
        view.show()
        view.grab()  # forces a paint pass through both chip + fallback branches

    def test_size_hint_grows_for_chips(self, qtbot) -> None:
        from PySide6.QtWidgets import QTableView

        view = QTableView()
        model = _OneColumnModel()
        model.set_rows([("running", "success")])
        view.setModel(model)
        delegate = StatusChipDelegate(view)
        qtbot.addWidget(view)
        from PySide6.QtWidgets import QStyleOptionViewItem

        option = QStyleOptionViewItem()
        hint = delegate.sizeHint(option, model.index(0, 0))
        assert hint.width() > 0 and hint.height() > 0
