"""Tests for pyos/Application.py — subscribe/unsubscribe, dispatch, lifecycle, exceptions."""

import pytest

from pyos.Activity import Activity
from pyos.Application import Application, Segue
from pyos.EventTypes import (
    ExceptionOccured,
    KeyStroke,
    ScrollChange,
    StopApplication,
    TextBoxSubmit,
)
from pyos.testing import HarnessApplication


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

class RecordingActivity(Activity):
    """Activity that records lifecycle calls."""

    def __init__(self, name="recording"):
        super().__init__()
        self.name = name
        self.started = False
        self.stopped = False
        self.start_count = 0
        self.stop_count = 0

    def on_start(self):
        self.started = True
        self.start_count += 1
        # Provide minimal display_state so refresh_screen won't crash
        self.display_state = {}

    def on_stop(self):
        self.stopped = True
        self.stop_count += 1


class CrashOnStopActivity(Activity):
    """Activity that raises in on_stop."""

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

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


# ===========================================================================
# subscribe / unsubscribe_all
# ===========================================================================

class TestSubscribe:
    def test_subscribe_and_receive_event(self, app):
        received = []

        def handler(event):
            received.append(event)

        activity = RecordingActivity()
        app.subscribe(ScrollChange, activity, handler)
        event = ScrollChange("test")
        app.dispatch_event(event)
        app.drain()
        assert len(received) == 1
        assert received[0] is event

    def test_two_callbacks_same_event_type(self, app):
        results = []

        def h1(event):
            results.append("h1")

        def h2(event):
            results.append("h2")

        a1 = RecordingActivity()
        a2 = RecordingActivity()
        app.subscribe(ScrollChange, a1, h1)
        app.subscribe(ScrollChange, a2, h2)
        app.dispatch_event(ScrollChange("x"))
        app.drain()
        assert "h1" in results
        assert "h2" in results

    def test_unsubscribe_all_removes_callbacks(self, app):
        received = []
        activity = RecordingActivity()
        app.subscribe(ScrollChange, activity, lambda e: received.append(e))
        app.unsubscribe_all(activity)
        app.dispatch_event(ScrollChange("x"))
        app.drain()
        assert len(received) == 0

    def test_unsubscribe_all_no_subscriptions_is_noop(self, app):
        activity = RecordingActivity()
        app.unsubscribe_all(activity)  # should not crash

    def test_unsubscribe_all_twice_is_idempotent(self, app):
        activity = RecordingActivity()
        app.subscribe(ScrollChange, activity, lambda e: None)
        app.unsubscribe_all(activity)
        app.unsubscribe_all(activity)  # second call should not crash


# ===========================================================================
# dispatch_event
# ===========================================================================

class TestDispatchEvent:
    def test_dispatch_with_no_subscribers(self, app):
        """Dispatching event with no subscribers should not crash."""
        app.dispatch_event(TextBoxSubmit(ui_element="none"))
        app.drain()

    def test_subscriber_that_raises_posts_exception(self, app):
        err = RuntimeError("boom")
        exceptions = []

        def exploding_handler(event):
            raise err

        def capture_exception(event):
            exceptions.append(event)

        activity = RecordingActivity()
        app.subscribe(ScrollChange, activity, exploding_handler)
        # The app's exception handler is already subscribed in setup
        app.dispatch_event(ScrollChange("x"))
        app.drain()
        # The exception handler wraps the error and posts ExceptionOccured

    def test_dispatch_unknown_event_type(self, app):
        class FakeEvent:
            pass

        app.dispatch_event(FakeEvent())
        app.drain()  # should not crash


# ===========================================================================
# Activity lifecycle — segue_to / pop_activity
# ===========================================================================

class TestActivityLifecycle:
    def test_push_adds_to_stack_and_starts(self, app):
        a = RecordingActivity("a")
        app.start_activity(a)
        assert len(app.stack) == 1
        assert a.started

    def test_replace_removes_old_starts_new(self, app):
        a = RecordingActivity("a")
        b = RecordingActivity("b")
        app.start_activity(a)
        app.start_activity(b, segue_type=Segue.REPLACE)
        assert len(app.stack) == 1
        assert a.stopped
        assert b.started

    def test_replace_on_empty_stack_is_like_push(self, app):
        a = RecordingActivity("a")
        app.start_activity(a, segue_type=Segue.REPLACE)
        assert len(app.stack) == 1
        assert a.started

    def test_pop_on_single_item_posts_stop_application(self, app):
        a = RecordingActivity("a")
        app.start_activity(a)
        app._pop_activity()
        stops = app.flush_stop_events()
        assert len(stops) >= 1

    def test_pop_on_two_items_restarts_returning(self, app):
        a = RecordingActivity("a")
        b = RecordingActivity("b")
        app.start_activity(a)
        app.start_activity(b)
        assert len(app.stack) == 2
        app._pop_activity()
        assert len(app.stack) == 1
        assert b.stopped
        assert a.start_count == 2  # started again when returning

    def test_pop_on_empty_stack_raises(self, app):
        with pytest.raises(IndexError):
            app._pop_activity()

    def test_on_start_called_on_push(self, app):
        a = RecordingActivity()
        app.start_activity(a)
        assert a.start_count == 1

    def test_on_stop_called_on_pop(self, app):
        a = RecordingActivity()
        b = RecordingActivity()
        app.start_activity(a)
        app.start_activity(b)
        app._pop_activity()
        assert b.stop_count == 1


# ===========================================================================
# Exception handling
# ===========================================================================

class TestExceptionHandling:
    def test_first_exception_pushes_show_exception_activity(self, app):
        a = RecordingActivity()
        app.start_activity(a)
        err = RuntimeError("test error")
        app.on_exception(ExceptionOccured(exception=err))
        app.drain()
        assert app.last_exception is err
        # ShowExceptionActivity should be pushed (2 items on stack now)
        assert len(app.stack) >= 2

    def test_second_exception_posts_stop_application(self, app):
        a = RecordingActivity()
        app.start_activity(a)
        app.last_exception = RuntimeError("first")
        app.on_exception(ExceptionOccured(exception=RuntimeError("second")))
        stops = app.flush_stop_events()
        assert len(stops) >= 1


# ===========================================================================
# handle_shutdown
# ===========================================================================

class TestHandleShutdown:
    def test_shutdown_no_exception(self, app):
        app.handle_shutdown(StopApplication())  # should not crash

    def test_shutdown_with_exception(self, app):
        app.handle_shutdown(StopApplication(exception=RuntimeError("bye")))

    def test_shutdown_when_main_thread_none(self, app):
        app.main_thread = None
        app.background_thread = None
        app.handle_shutdown(StopApplication())  # should not crash
