"""Tests for pyos/Activity.py — lifecycle, generate_line_printers, refresh_screen."""

import pytest
from functools import partial

from pyos.Activity import Activity
from pyos.Application import Segue
from pyos.printers.printers import print_line


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

class MinimalActivity(Activity):
    """Activity with empty display_state for basic lifecycle tests."""

    def on_start(self):
        self.display_state = {}


class DisplayActivity(Activity):
    """Activity with a display_state containing a fixed-height line generator."""

    def on_start(self):
        self.display_state = {
            "header": {
                "layout": {"height": 1},
                "line_generator": lambda ctx, h: [partial(print_line, 0, "Header")],
            },
        }


class OnStopCrashActivity(Activity):
    """Activity whose on_stop raises."""

    def on_start(self):
        self.display_state = {}

    def on_stop(self):
        raise RuntimeError("on_stop crash")


# ===========================================================================
# _stop lifecycle
# ===========================================================================

class TestStop:
    def test_stop_sets_lifecycle_stopped(self, app):
        a = MinimalActivity()
        app.start_activity(a)
        assert a.lifecycle_state == "started"
        a._stop()
        assert a.lifecycle_state == "stopped"

    def test_stop_clears_application_ref(self, app):
        a = MinimalActivity()
        app.start_activity(a)
        assert a.application is not None
        a._stop()
        assert a.application is None

    def test_on_stop_called_during_stop(self, app):
        called = []

        class TrackStop(Activity):
            def on_start(self):
                self.display_state = {}

            def on_stop(self):
                called.append(True)

        a = TrackStop()
        app.start_activity(a)
        a._stop()
        assert len(called) == 1

    def test_on_stop_exception_lifecycle_not_updated(self, app):
        """If on_stop raises, lifecycle_state is NOT set to 'stopped' (documents behavior)."""
        a = OnStopCrashActivity()
        app.start_activity(a)
        with pytest.raises(RuntimeError, match="on_stop crash"):
            a._stop()
        # lifecycle_state stays at "started" because the exception happens before assignment
        assert a.lifecycle_state == "started"


# ===========================================================================
# refresh_screen
# ===========================================================================

class TestRefreshScreen:
    def test_refresh_skipped_when_stopped(self, app):
        a = DisplayActivity()
        app.start_activity(a)
        initial_count = app.curses_screen.refresh_count
        a._stop()
        # After stop, lifecycle_state is "stopped", so refresh does nothing
        # But application is None so it would crash anyway — lifecycle guard protects
        a.lifecycle_state = "stopped"
        # refresh_screen should be a no-op (won't call generate_line_printers)
        # We can't call it directly since application is None and it would crash
        # The guard `if self.lifecycle_state != "stopped"` prevents the body from running

    def test_refresh_screen_renders(self, app):
        a = DisplayActivity()
        app.start_activity(a)
        screen = app.curses_screen
        assert screen.find_text("Header")


# ===========================================================================
# generate_line_printers
# ===========================================================================

class TestGenerateLinePrinters:
    def test_empty_display_state(self, app):
        a = MinimalActivity()
        app.start_activity(a)
        printers = a.generate_line_printers()
        assert printers == []

    def test_missing_layout_key_raises(self, app):
        a = MinimalActivity()
        app.start_activity(a)
        a.display_state = {"bad": {"text": "no layout"}}
        with pytest.raises(Exception, match="Legacy display_state item detected without 'layout'"):
            a.generate_line_printers()

    def test_mixed_constraint_and_dict_raises(self, app):
        from pyos.layout import Constraint, Length

        a = MinimalActivity()
        app.start_activity(a)
        a.display_state = {
            "c1": {
                "layout": Length(3),
                "line_generator": lambda ctx, h: [],
            },
            "d1": {
                "layout": {"height": 2},
                "line_generator": lambda ctx, h: [],
            },
        }
        with pytest.raises(TypeError, match="Cannot mix Constraint layouts with dict layouts"):
            a.generate_line_printers()

    def test_line_generator_truncated_to_allocated(self, app):
        """A line generator that returns more lines than allocated gets truncated."""

        def many_lines(ctx, h):
            return [partial(print_line, 0, f"line{i}") for i in range(100)]

        a = MinimalActivity()
        app.start_activity(a)
        a.display_state = {
            "big": {
                "layout": {"height": 3},
                "line_generator": many_lines,
            },
        }
        printers = a.generate_line_printers()
        assert len(printers) == 3

    def test_line_generator_exception_propagated(self, app):
        """Line generator that raises → exception propagated."""

        def exploding_gen(ctx, h):
            raise ValueError("generator boom")

        a = MinimalActivity()
        app.start_activity(a)
        a.display_state = {
            "boom": {
                "layout": {"height": 2},
                "line_generator": exploding_gen,
            },
        }
        with pytest.raises(Exception, match="Problem with line generator"):
            a.generate_line_printers()
