"""Tests for the Service lifecycle system."""

import threading
import time

import pytest

from pyos.Activity import Activity
from pyos.Service import Service, ServiceState
from pyos.testing import HarnessApplication, MockScreen


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

class DummyService(Service):
    """Minimal service that tracks lifecycle calls."""

    def __init__(self):
        super().__init__()
        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

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


class SlowService(Service):
    """Service whose on_start takes a noticeable amount of time."""

    def __init__(self, delay=0.2):
        super().__init__()
        self.delay = delay

    def on_start(self):
        time.sleep(self.delay)

    def on_stop(self):
        pass


class FailingStartService(Service):
    """Service that raises during on_start."""

    def on_start(self):
        raise RuntimeError("start failed")

    def on_stop(self):
        pass


class FailingStopService(Service):
    """Service that raises during on_stop."""

    def on_start(self):
        pass

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


class ServiceUsingActivity(Activity):
    """Activity that calls require_service in on_start."""

    def __init__(self, service_name):
        super().__init__()
        self.service_name = service_name
        self.acquired_service = None

    def on_start(self):
        self.acquired_service = self.require_service(self.service_name, timeout=5)
        self.display_state = {}

    def on_stop(self):
        pass


@pytest.fixture
def app():
    screen = MockScreen(24, 80)
    application = HarnessApplication(screen)
    application.setup()
    yield application
    application.teardown()


# ===========================================================================
# Registration
# ===========================================================================

class TestRegistration:
    def test_register_and_retrieve(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        assert app.service("dummy") is svc

    def test_register_sets_application(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        assert svc.application is app

    def test_register_duplicate_name_raises(self, app):
        app.register_service("dummy", DummyService())
        with pytest.raises(ValueError, match="already registered"):
            app.register_service("dummy", DummyService())

    def test_retrieve_unregistered_raises(self, app):
        with pytest.raises(KeyError):
            app.service("nonexistent")

    def test_initial_state_is_idle(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        assert svc.state == ServiceState.IDLE
        assert not svc.is_running


# ===========================================================================
# Start / Stop
# ===========================================================================

class TestStartStop:
    def test_start_service_reaches_running(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        app.start_service_sync("dummy")
        assert svc.state == ServiceState.RUNNING
        assert svc.is_running
        assert svc.started
        assert svc.start_count == 1

    def test_stop_service_reaches_stopped(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        app.start_service_sync("dummy")
        app.stop_service_sync("dummy")
        assert svc.state == ServiceState.STOPPED
        assert not svc.is_running
        assert svc.stopped
        assert svc.stop_count == 1

    def test_start_already_running_raises(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        app.start_service_sync("dummy")
        with pytest.raises(RuntimeError, match="Cannot start"):
            app.start_service("dummy")

    def test_stop_not_running_raises(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        with pytest.raises(RuntimeError, match="Cannot stop"):
            app.stop_service("dummy")

    def test_start_after_stopped(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        app.start_service_sync("dummy")
        app.stop_service_sync("dummy")
        app.start_service_sync("dummy")
        assert svc.state == ServiceState.RUNNING
        assert svc.start_count == 2

    def test_wait_until_running(self, app):
        svc = SlowService(delay=0.1)
        app.register_service("slow", svc)
        app.start_service("slow")
        assert svc.wait_until_running(timeout=5)
        assert svc.state == ServiceState.RUNNING


# ===========================================================================
# Callbacks
# ===========================================================================

class TestCallbacks:
    def test_on_started_callback(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        callback_called = []

        def on_started():
            callback_called.append(True)

        app.start_service("dummy", on_started=on_started)
        svc.wait_until_running(timeout=5)
        app.drain()
        assert len(callback_called) == 1

    def test_on_stopped_callback(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        app.start_service_sync("dummy")

        callback_called = []

        def on_stopped():
            callback_called.append(True)

        app.stop_service("dummy", on_stopped=on_stopped)
        svc.wait_until_stopped(timeout=5)
        app.drain()
        assert len(callback_called) == 1


# ===========================================================================
# Restart
# ===========================================================================

class TestRestart:
    def test_restart_running_service(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        app.start_service_sync("dummy")
        assert svc.start_count == 1

        future = app.restart_service("dummy")
        svc.wait_until_running(timeout=5)
        app.drain()
        assert svc.state == ServiceState.RUNNING
        assert svc.start_count == 2
        assert svc.stop_count == 1

    def test_restart_stopped_service(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        app.start_service_sync("dummy")
        app.stop_service_sync("dummy")

        future = app.restart_service("dummy")
        svc.wait_until_running(timeout=5)
        app.drain()
        assert svc.state == ServiceState.RUNNING
        assert svc.start_count == 2

    def test_restart_with_callback(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        app.start_service_sync("dummy")

        callback_called = []
        future = app.restart_service("dummy", on_started=lambda: callback_called.append(True))
        svc.wait_until_running(timeout=5)
        app.drain()
        assert len(callback_called) == 1


# ===========================================================================
# require_service
# ===========================================================================

class TestRequireService:
    def test_require_auto_starts_idle_service(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        result = app.require_service("dummy", timeout=5)
        assert result is svc
        assert svc.state == ServiceState.RUNNING

    def test_require_returns_already_running(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        app.start_service_sync("dummy")
        result = app.require_service("dummy", timeout=5)
        assert result is svc
        assert svc.start_count == 1  # not started again

    def test_require_from_activity(self, app):
        svc = DummyService()
        app.register_service("dummy", svc)
        activity = ServiceUsingActivity("dummy")
        app.start_activity(activity)
        assert activity.acquired_service is svc
        assert svc.state == ServiceState.RUNNING


# ===========================================================================
# Shutdown cleanup
# ===========================================================================

class TestShutdownCleanup:
    def test_teardown_stops_running_services(self):
        screen = MockScreen(24, 80)
        application = HarnessApplication(screen)
        application.setup()

        svc = DummyService()
        application.register_service("dummy", svc)
        application.start_service_sync("dummy")
        assert svc.state == ServiceState.RUNNING

        application.teardown()
        assert svc.state == ServiceState.STOPPED
        assert svc.stop_count == 1

    def test_shutdown_stops_multiple_services(self):
        screen = MockScreen(24, 80)
        application = HarnessApplication(screen)
        application.setup()

        svc1 = DummyService()
        svc2 = DummyService()
        application.register_service("svc1", svc1)
        application.register_service("svc2", svc2)
        application.start_service_sync("svc1")
        application.start_service_sync("svc2")

        application.teardown()
        assert svc1.state == ServiceState.STOPPED
        assert svc2.state == ServiceState.STOPPED

    def test_shutdown_ignores_idle_services(self):
        screen = MockScreen(24, 80)
        application = HarnessApplication(screen)
        application.setup()

        svc = DummyService()
        application.register_service("dummy", svc)
        # never started
        application.teardown()
        assert svc.state == ServiceState.IDLE
        assert svc.stop_count == 0


# ===========================================================================
# Error handling
# ===========================================================================

class TestErrorHandling:
    def test_start_failure_sets_stopped(self, app):
        svc = FailingStartService()
        app.register_service("bad", svc)
        future = app.start_service("bad")
        svc.wait_until_stopped(timeout=5)
        assert svc.state == ServiceState.STOPPED
        assert not svc.is_running

    def test_stop_failure_still_marks_stopped(self, app):
        svc = FailingStopService()
        app.register_service("bad", svc)
        app.start_service_sync("bad")
        future = app.stop_service("bad")
        svc.wait_until_stopped(timeout=5)
        assert svc.state == ServiceState.STOPPED


# ===========================================================================
# State machine
# ===========================================================================

class TestStateMachine:
    def test_full_lifecycle(self, app):
        """IDLE -> STARTING -> RUNNING -> STOPPING -> STOPPED"""
        svc = SlowService(delay=0.1)
        app.register_service("svc", svc)

        assert svc.state == ServiceState.IDLE

        app.start_service("svc")
        svc.wait_until_running(timeout=5)
        assert svc.state == ServiceState.RUNNING

        app.stop_service("svc")
        svc.wait_until_stopped(timeout=5)
        assert svc.state == ServiceState.STOPPED

    def test_concurrent_service_starts(self, app):
        """Multiple services can start concurrently."""
        svc1 = SlowService(delay=0.1)
        svc2 = SlowService(delay=0.1)
        app.register_service("svc1", svc1)
        app.register_service("svc2", svc2)

        app.start_service("svc1")
        app.start_service("svc2")

        assert svc1.wait_until_running(timeout=5)
        assert svc2.wait_until_running(timeout=5)
        assert svc1.state == ServiceState.RUNNING
        assert svc2.state == ServiceState.RUNNING


# ===========================================================================
# Concurrency stress tests
# ===========================================================================

class CountingService(Service):
    """Service that counts how many times on_start/on_stop actually ran."""

    def __init__(self, delay=0.05):
        super().__init__()
        self.delay = delay
        self.start_count = 0
        self.stop_count = 0
        self._lock = threading.Lock()

    def on_start(self):
        time.sleep(self.delay)
        with self._lock:
            self.start_count += 1

    def on_stop(self):
        time.sleep(self.delay)
        with self._lock:
            self.stop_count += 1


class TestConcurrencyStress:
    def test_double_start_rejected(self, app):
        """Calling start_service twice must not run on_start twice."""
        svc = CountingService(delay=0.1)
        app.register_service("svc", svc)

        app.start_service("svc")
        # Second call should see STARTING and reject.
        with pytest.raises(RuntimeError, match="Cannot start"):
            app.start_service("svc")

        svc.wait_until_running(timeout=5)
        assert svc.start_count == 1

    def test_double_stop_rejected(self, app):
        """Calling stop_service twice must not run on_stop twice."""
        svc = CountingService(delay=0.1)
        app.register_service("svc", svc)
        app.start_service_sync("svc")

        app.stop_service("svc")
        # Second call should see STOPPING and reject.
        with pytest.raises(RuntimeError, match="Cannot stop"):
            app.stop_service("svc")

        svc.wait_until_stopped(timeout=5)
        assert svc.stop_count == 1

    def test_start_during_stopping_rejected(self, app):
        """Cannot start a service that is mid-stop."""
        svc = CountingService(delay=0.15)
        app.register_service("svc", svc)
        app.start_service_sync("svc")

        app.stop_service("svc")
        assert svc.state == ServiceState.STOPPING

        with pytest.raises(RuntimeError, match="Cannot start"):
            app.start_service("svc")

    def test_stop_during_starting_rejected(self, app):
        """Cannot stop a service that is mid-start."""
        svc = CountingService(delay=0.15)
        app.register_service("svc", svc)

        app.start_service("svc")
        assert svc.state == ServiceState.STARTING

        with pytest.raises(RuntimeError, match="Cannot stop"):
            app.stop_service("svc")

    def test_restart_during_starting_rejected(self, app):
        """Cannot restart a service that is mid-start."""
        svc = CountingService(delay=0.15)
        app.register_service("svc", svc)

        app.start_service("svc")
        assert svc.state == ServiceState.STARTING

        with pytest.raises(RuntimeError, match="Cannot restart"):
            app.restart_service("svc")

    def test_restart_during_stopping_rejected(self, app):
        """Cannot restart a service that is mid-stop."""
        svc = CountingService(delay=0.15)
        app.register_service("svc", svc)
        app.start_service_sync("svc")

        app.stop_service("svc")
        assert svc.state == ServiceState.STOPPING

        with pytest.raises(RuntimeError, match="Cannot restart"):
            app.restart_service("svc")

    def test_rapid_start_stop_cycles(self, app):
        """Rapid start/stop cycles settle in a valid terminal state."""
        svc = CountingService(delay=0.02)
        app.register_service("svc", svc)

        for _ in range(10):
            app.start_service_sync("svc")
            app.stop_service_sync("svc")

        assert svc.state == ServiceState.STOPPED
        assert svc.start_count == 10
        assert svc.stop_count == 10

    def test_rapid_restart_cycles(self, app):
        """Rapid restart cycles each complete a full stop+start."""
        svc = CountingService(delay=0.02)
        app.register_service("svc", svc)
        app.start_service_sync("svc")

        for _ in range(5):
            future = app.restart_service("svc")
            svc.wait_until_running(timeout=5)
            app.drain()

        assert svc.state == ServiceState.RUNNING
        assert svc.start_count == 6   # 1 initial + 5 restarts
        assert svc.stop_count == 5

    def test_many_concurrent_services(self, app):
        """Start and stop many services concurrently."""
        services = {}
        for i in range(10):
            svc = CountingService(delay=0.02)
            name = f"svc-{i}"
            app.register_service(name, svc)
            services[name] = svc

        for name in services:
            app.start_service(name)

        for name, svc in services.items():
            assert svc.wait_until_running(timeout=5), f"{name} did not start"

        for name in services:
            app.stop_service(name)

        for name, svc in services.items():
            assert svc.wait_until_stopped(timeout=5), f"{name} did not stop"
            assert svc.start_count == 1
            assert svc.stop_count == 1

    def test_immediate_state_transition_on_start(self, app):
        """State must be STARTING before the future thread even runs."""
        svc = CountingService(delay=0.2)
        app.register_service("svc", svc)

        app.start_service("svc")
        # Immediately after the call — no sleeping.
        assert svc.state == ServiceState.STARTING

        svc.wait_until_running(timeout=5)

    def test_immediate_state_transition_on_stop(self, app):
        """State must be STOPPING before the future thread even runs."""
        svc = CountingService(delay=0.2)
        app.register_service("svc", svc)
        app.start_service_sync("svc")

        app.stop_service("svc")
        # Immediately after the call — no sleeping.
        assert svc.state == ServiceState.STOPPING

        svc.wait_until_stopped(timeout=5)

    def test_shutdown_waits_for_starting_service(self):
        """_stop_all_services must not skip a service that is mid-start."""
        screen = MockScreen(24, 80)
        application = HarnessApplication(screen)
        application.setup()

        svc = CountingService(delay=0.15)
        application.register_service("svc", svc)
        application.start_service("svc")
        assert svc.state == ServiceState.STARTING

        application.teardown()
        assert svc.state == ServiceState.STOPPED
        assert svc.start_count == 1
        assert svc.stop_count == 1

    def test_callbacks_fire_exactly_once_under_stress(self, app):
        """Callbacks from start/stop must fire exactly once per call."""
        svc = CountingService(delay=0.02)
        app.register_service("svc", svc)

        started_calls = []
        stopped_calls = []

        for _ in range(5):
            app.start_service("svc", on_started=lambda: started_calls.append(1))
            svc.wait_until_running(timeout=5)
            app.drain()
            app.stop_service("svc", on_stopped=lambda: stopped_calls.append(1))
            svc.wait_until_stopped(timeout=5)
            app.drain()

        assert len(started_calls) == 5
        assert len(stopped_calls) == 5
