// SPDX-FileCopyrightText: 2025 Simon BrummeR
//
// SPDX-License-Identifier: MPL-2.0

#[cfg(all(test, not(feature = "tokio"), feature = "agnostic"))]
mod tests {
    use crate::error::mocks::MockError;
    use crate::integration::mocks::MockIntegration;
    use crate::{
        Daemon, MockConfigFormat, MockService, MockWatchdog, ReloadPolicy, RestartPolicy,
        ServiceConfig,
    };

    use async_timer::Interval;
    use futures::{FutureExt, join};
    use mockall::{Sequence, predicate::eq};
    use smol::block_on;
    use smol_timeout::TimeoutExt;
    use std::future::{pending, ready};
    use std::path::PathBuf;
    use std::time::Duration;

    /// Setup a mock that with a single use reload trigger.
    /// On usage, the first usage continues immediately, the next invocation blocks forever.
    /// The mock expects that wait_for_reload is called two times.
    fn mock_single_use_reload_trigger() -> (MockIntegration, Sequence) {
        let mut sequence = Sequence::new();
        let mut mock = MockIntegration::new();
        mock.expect_wait_for_reload()
            .times(1)
            .in_sequence(&mut sequence)
            .returning(|| Box::pin(ready(())));
        mock.expect_wait_for_reload()
            .times(1)
            .in_sequence(&mut sequence)
            .returning(|| Box::pin(pending()));
        (mock, sequence)
    }

    /// Setup a mock were the first config file loading is successfully and the next attempt fails.
    /// The mock expects that try_load is called two times.
    fn mock_single_use_config_loading(config: &'static str) -> (MockConfigFormat, Sequence) {
        let mut sequence = Sequence::new();
        let mut mock = MockConfigFormat::new();
        mock.expect_try_load()
            .times(1)
            .in_sequence(&mut sequence)
            .returning(|_| Ok(config.to_string()));
        mock.expect_try_load()
            .times(1)
            .in_sequence(&mut sequence)
            .returning(|_| Err(MockError.into()));
        (mock, sequence)
    }

    /// Expected behavior:
    /// A signal is send after the given function implementation leaves the function.
    #[test]
    fn shutdown_trigger() {
        block_on(async {
            let mut mock = MockIntegration::new();
            mock.expect_wait_for_shutdown()
                .times(1)
                .returning(|| Box::pin(ready(())));
            mock.expect_notify_stopping().times(1).returning(|| ());

            let mocked_daemon = Daemon::new(mock);

            assert!(!mocked_daemon.shutdown.is_canceled());
            mocked_daemon.shutdown_handler().await;
            assert!(mocked_daemon.shutdown.is_canceled());
        });
    }

    /// Expected behavior:
    /// The shutdown trigger is not the only source for global shutdown. In case the
    /// cancellation token is triggered externally, a blocking shutdown handler must terminate.
    #[test]
    fn shutdown_external_trigger() {
        block_on(async {
            let mut mock = MockIntegration::new();
            mock.expect_wait_for_shutdown()
                .times(1)
                .returning(|| Box::pin(pending()));

            let mocked_daemon = Daemon::new(mock);

            join!(mocked_daemon.shutdown_handler(), async {
                mocked_daemon.shutdown.cancel();
            })
        });
    }

    /// Expected behavior:
    /// A watchdog shuts down stopped by a shutdown signal.
    #[test]
    fn watchdog_shutdown() {
        block_on(async {
            // 1) Unblock by signal
            let mut mock = MockWatchdog::new();
            mock.expect_notify_interval().times(1).returning(|| None);
            mock.expect_notify().times(0).returning(|| ());

            let mocked_daemon = Daemon::<MockWatchdog>::new(mock);

            join!(mocked_daemon.watchdog_handler(), async {
                mocked_daemon.shutdown.cancel();
            });
        });
    }

    /// Expected behavior:
    /// - A disabled watchdog must never call notify().
    /// - A disabled watchdog must not terminate on its own
    ///   it can only be interrupted.
    #[test]
    fn disabled_watchdog_is_pending() {
        block_on(async {
            let mut mock = MockWatchdog::new();
            mock.expect_notify_interval().times(1).returning(|| None);
            mock.expect_notify().times(0).returning(|| ());

            let mocked_daemon = Daemon::<MockWatchdog>::new(mock);

            assert!(
                mocked_daemon
                    .watchdog_handler()
                    .timeout(Duration::from_millis(100))
                    .await
                    .is_none()
            );
        });
    }

    /// Expected behavior:
    /// - A enable watchdog must call notify() on start and keep triggering it the given interval.
    /// - A disabled watchdog must not terminate on its own
    ///   it can only be interrupted.
    #[test]
    fn enabled_watchdog_is_triggering() {
        block_on(async {
            // The actual notify interval is halfed minus some offset.
            // Using 50ms means an effective trigger of ~24.9ms.
            // If running for 100ms that makes 4 trigger executions

            let mut mock = MockWatchdog::new();
            mock.expect_notify_interval()
                .times(1)
                .returning(|| Some(Duration::from_millis(50)));
            mock.expect_notify().times(4..).returning(|| ());

            let mocked_daemon = Daemon::<MockWatchdog>::new(mock);

            assert!(
                mocked_daemon
                    .watchdog_handler()
                    .timeout(Duration::from_millis(100))
                    .await
                    .is_none()
            );
        });
    }

    /// Expected behavior:
    /// After a reload trigger was received and a following configuration file loading fails, the
    /// `ReloadPolicy::KeepOnError` ensures that the current, working configuration is kept.
    #[test]
    fn reload_handler_reload_policy_keep_on_error() {
        block_on(async {
            let (mut reload_trigger_mock, _) = mock_single_use_reload_trigger();
            reload_trigger_mock
                .expect_notify_reloading()
                .times(1)
                .returning(|| ());

            let config_data = "non_default_config";
            let (config_loading_mock, _) = mock_single_use_config_loading(config_data);
            let config = ServiceConfig::try_load(&PathBuf::new(), config_loading_mock).unwrap();
            let daemon = Daemon::new(reload_trigger_mock);

            let mut config_update_receiver = config.receiver().clone();
            let config_received = futures::select! {
                _ = config_update_receiver.recv().fuse() => true,
                _ = daemon.reload_handler(&config, ReloadPolicy::KeepOnError).fuse() => false,
            };
            assert!(
                config_received,
                "reload_handler must only return on shutdown token cancellation."
            );
            assert_eq!(config.get(), config_data);
        });
    }

    /// Expected behavior:
    /// After a reload trigger was received and a following configuration file loading fails, the
    /// `ReloadPolicy::DefaultOnError` replaces the current configuration with the default
    /// configuration. The underlying config file must not be rewritten.
    #[test]
    fn reload_handler_reload_policy_default_on_error() {
        block_on(async {
            let (mut reload_trigger_mock, _) = mock_single_use_reload_trigger();
            reload_trigger_mock
                .expect_notify_reloading()
                .times(1)
                .returning(|| ());

            let (config_loading_mock, _) = mock_single_use_config_loading("non_default_config");
            let config = ServiceConfig::try_load(&PathBuf::new(), config_loading_mock).unwrap();
            let daemon = Daemon::new(reload_trigger_mock);

            let mut config_update_receiver = config.receiver().clone();
            let config_received = futures::select! {
                _ = config_update_receiver.recv().fuse() => true,
                _ = daemon.reload_handler(&config, ReloadPolicy::DefaultOnError).fuse() => false,
            };
            assert!(
                config_received,
                "reload_handler must only return on shutdown token cancellation."
            );
            assert_eq!(config.get(), String::default());
        });
    }

    /// Expected behavior:
    /// After a reload trigger was received and a following configuration file loading fails, the
    /// `ReloadPolicy::ShutdownOnError` initiates global teardown by setting the shutdown token
    /// instead. The underlying config file must not be rewritten and no new configuration is
    /// propagated.
    #[test]
    fn reload_handler_reload_policy_shutdown_on_error() {
        block_on(async {
            let mut reload_trigger_mock = MockIntegration::new();
            reload_trigger_mock
                .expect_wait_for_reload()
                .times(1)
                .returning(|| Box::pin(ready(())));
            reload_trigger_mock
                .expect_notify_stopping()
                .times(1)
                .returning(|| ());

            let (config_loading_mock, _) = mock_single_use_config_loading("non_default_config");
            let config = ServiceConfig::try_load(&PathBuf::new(), config_loading_mock).unwrap();
            let daemon = Daemon::new(reload_trigger_mock);

            let mut config_update_receiver = config.receiver().clone();
            let config_received = futures::select! {
                _ = config_update_receiver.recv().fuse() => true,
                _ = daemon.reload_handler(&config, ReloadPolicy::ShutdownOnError).fuse() => false,
            };
            assert!(
                !config_received,
                "new configurations must not be received on shutdown."
            );
            assert!(daemon.shutdown.is_canceled());
        });
    }

    // Expected behavior:
    // The reload handler shall return after the daemons cancellation token was triggered.
    #[test]
    fn reload_handler_shutdown() {
        block_on(async {
            let mut reload_trigger_mock = MockIntegration::new();
            reload_trigger_mock
                .expect_wait_for_reload()
                .times(1)
                .returning(|| Box::pin(pending()));

            let mut config_loading_mock = MockConfigFormat::new();
            config_loading_mock
                .expect_try_load()
                .returning(|_| Ok(String::default()));

            let config = ServiceConfig::try_load(&PathBuf::new(), config_loading_mock).unwrap();
            let daemon = Daemon::new(reload_trigger_mock);

            join!(
                daemon.reload_handler(&config, ReloadPolicy::KeepOnError),
                async {
                    daemon.shutdown.cancel();
                }
            );
        });
    }

    // Expected behavior:
    // A reload trigger distributes the latest configuration in case it could be loaded.
    #[test]
    fn reload_handler_reload_successfully() {
        block_on(async {
            // Prepare a reload trigger
            let mut reload_sequence = Sequence::new();
            let mut reload_trigger_mock = MockIntegration::new();
            reload_trigger_mock
                .expect_wait_for_reload()
                .times(1)
                .in_sequence(&mut reload_sequence)
                .returning(|| Box::pin(ready(())));
            reload_trigger_mock
                .expect_notify_reloading()
                .times(1)
                .in_sequence(&mut reload_sequence)
                .returning(|| ());
            reload_trigger_mock
                .expect_wait_for_reload()
                .times(1)
                .in_sequence(&mut reload_sequence)
                .returning(|| Box::pin(pending()));

            // Prepare two subsequent different configuration contents
            let config1 = "Config1";
            let config2 = "Config2";
            let mut config_loading_sequence = Sequence::new();
            let mut config_loading_mock = MockConfigFormat::new();
            config_loading_mock
                .expect_try_load()
                .times(1)
                .in_sequence(&mut config_loading_sequence)
                .returning(|_| Ok(config1.to_string()));
            config_loading_mock
                .expect_try_load()
                .times(1)
                .in_sequence(&mut config_loading_sequence)
                .returning(|_| Ok(config2.to_string()));

            // Spawn daemon
            let daemon = Daemon::new(reload_trigger_mock);
            let config = ServiceConfig::try_load(&PathBuf::new(), config_loading_mock).unwrap();

            assert_eq!(config.get(), config1);
            let mut config_update_receiver = config.receiver().clone();
            let config_received = futures::select! {
                _ = config_update_receiver.recv().fuse() => true,
                _ = daemon.reload_handler(&config, ReloadPolicy::DefaultOnError).fuse() => false,
            };
            assert!(
                config_received,
                "reload_handler must only return on shutdown token cancellation."
            );
            assert_eq!(config.get(), config2);
            assert!(!daemon.shutdown.is_canceled());
        });
    }

    // Expected behavior:
    // A service is always restarted on an okay result, service_handler
    // does not return.
    #[test]
    fn service_handler_restart_policy_always_service_returns_ok() {
        let mut config_mock = MockConfigFormat::new();
        config_mock
            .expect_try_load()
            .times(1)
            .returning(|_| Ok(String::new()));
        let config = ServiceConfig::try_load(&PathBuf::new(), config_mock).unwrap();

        block_on(async {
            let mut sequence = Sequence::new();
            let mut service_mock = MockService::new();
            service_mock
                .expect_run()
                .times(1)
                .in_sequence(&mut sequence)
                .returning(|_| Box::pin(ready(Ok(()))));
            service_mock
                .expect_run()
                .times(1)
                .in_sequence(&mut sequence)
                .returning(|_| Box::pin(pending()));

            let mut integration_mock = MockIntegration::new();
            integration_mock
                .expect_notify_ready()
                .times(2)
                .returning(|| ());

            let daemon = Daemon::new(integration_mock);
            assert!(
                daemon
                    .service_handler(&service_mock, &config, RestartPolicy::Always)
                    .timeout(Duration::from_millis(100))
                    .await
                    .is_none()
            );
            assert!(!daemon.shutdown.is_canceled());
        })
    }

    // Expected behavior:
    // A service is always restarted on an error result, service_handler
    // does not return.
    #[test]
    fn service_handler_restart_policy_always_service_returns_error() {
        let mut config_mock = MockConfigFormat::new();
        config_mock
            .expect_try_load()
            .times(1)
            .returning(|_| Ok(String::new()));
        let config = ServiceConfig::try_load(&PathBuf::new(), config_mock).unwrap();

        block_on(async {
            let mut sequence = Sequence::new();
            let mut service_mock = MockService::new();
            service_mock
                .expect_run()
                .times(1)
                .in_sequence(&mut sequence)
                .returning(|_| Box::pin(ready(Err(MockError))));
            service_mock
                .expect_run()
                .times(1)
                .in_sequence(&mut sequence)
                .returning(|_| Box::pin(pending()));

            let mut integration_mock = MockIntegration::new();
            integration_mock
                .expect_notify_ready()
                .times(2)
                .returning(|| ());

            let daemon = Daemon::new(integration_mock);
            assert!(
                daemon
                    .service_handler(&service_mock, &config, RestartPolicy::Always)
                    .timeout(Duration::from_millis(100))
                    .await
                    .is_none()
            );
            assert!(!daemon.shutdown.is_canceled());
        })
    }

    // Expected behavior:
    // A service is never restarted in case of an okay result. This means that service_handler returns on its own.
    // Before returning, the global shutdown token must be triggered and the shutdown is notified.
    #[test]
    fn service_handler_restart_policy_never_service_returns_okay() {
        let mut config_mock = MockConfigFormat::new();
        config_mock
            .expect_try_load()
            .times(1)
            .returning(|_| Ok(String::new()));
        let config = ServiceConfig::try_load(&PathBuf::new(), config_mock).unwrap();

        block_on(async {
            let mut service_mock = MockService::new();
            service_mock
                .expect_run()
                .times(1)
                .returning(|_| Box::pin(ready(Ok(()))));

            let mut integration_mock = MockIntegration::new();
            integration_mock
                .expect_notify_ready()
                .times(1)
                .returning(|| ());
            integration_mock
                .expect_notify_stopping()
                .times(1)
                .returning(|| ());

            let daemon = Daemon::new(integration_mock);
            assert!(
                daemon
                    .service_handler(&service_mock, &config, RestartPolicy::Never)
                    .timeout(Duration::from_millis(100))
                    .await
                    .is_some()
            );
            assert!(daemon.shutdown.is_canceled());
        });
    }

    // Expected behavior:
    // A service is never restarted in case of an error result. This means that service_handler returns on its own.
    // Before returning, the global shutdown token must be triggered and the shutdown is notified.
    #[test]
    fn service_handler_restart_policy_never_service_returns_error() {
        let mut config_mock = MockConfigFormat::new();
        config_mock
            .expect_try_load()
            .times(1)
            .returning(|_| Ok(String::new()));
        let config = ServiceConfig::try_load(&PathBuf::new(), config_mock).unwrap();

        block_on(async {
            let mut service_mock = MockService::new();
            service_mock
                .expect_run()
                .times(1)
                .returning(|_| Box::pin(ready(Err(MockError))));

            let mut integration_mock = MockIntegration::new();
            integration_mock
                .expect_notify_ready()
                .times(1)
                .returning(|| ());
            integration_mock
                .expect_notify_stopping()
                .times(1)
                .returning(|| ());

            let daemon = Daemon::new(integration_mock);
            assert!(
                daemon
                    .service_handler(&service_mock, &config, RestartPolicy::Never)
                    .timeout(Duration::from_millis(100))
                    .await
                    .is_some()
            );
            assert!(daemon.shutdown.is_canceled());
        });
    }

    // Expected behavior:
    // A service is restarted in case the result was okay, service_handler
    // does not return.
    #[test]
    fn service_handler_restart_policy_on_success_service_returns_okay() {
        let mut config_mock = MockConfigFormat::new();
        config_mock
            .expect_try_load()
            .times(1)
            .returning(|_| Ok(String::new()));
        let config = ServiceConfig::try_load(&PathBuf::new(), config_mock).unwrap();

        block_on(async {
            let mut service_mock = MockService::new();
            service_mock
                .expect_run()
                .times(2)
                .returning(|_| Box::pin(ready(Ok(()))));
            service_mock
                .expect_run()
                .times(1)
                .returning(|_| Box::pin(pending()));

            let mut integration_mock = MockIntegration::new();
            integration_mock
                .expect_notify_ready()
                .times(3)
                .returning(|| ());

            let daemon = Daemon::new(integration_mock);
            assert!(
                daemon
                    .service_handler(&service_mock, &config, RestartPolicy::OnSuccess)
                    .timeout(Duration::from_millis(100))
                    .await
                    .is_none()
            );
            assert!(!daemon.shutdown.is_canceled());
        });
    }

    // Expected behavior:
    // In case of an error, the service_handler returns on its own. Before
    // returning, the global shutdown token must be triggered and the shutdown is notified.
    #[test]
    fn service_handler_restart_policy_on_success_service_returns_error() {
        let mut config_mock = MockConfigFormat::new();
        config_mock
            .expect_try_load()
            .times(1)
            .returning(|_| Ok(String::new()));
        let config = ServiceConfig::try_load(&PathBuf::new(), config_mock).unwrap();

        block_on(async {
            let mut service_mock = MockService::new();
            service_mock
                .expect_run()
                .times(1)
                .returning(|_| Box::pin(ready(Err(MockError))));

            let mut integration_mock = MockIntegration::new();
            integration_mock
                .expect_notify_ready()
                .times(1)
                .returning(|| ());
            integration_mock
                .expect_notify_stopping()
                .times(1)
                .returning(|| ());

            let daemon = Daemon::new(integration_mock);
            assert!(
                daemon
                    .service_handler(&service_mock, &config, RestartPolicy::OnSuccess)
                    .timeout(Duration::from_millis(100))
                    .await
                    .is_some()
            );
            assert!(daemon.shutdown.is_canceled());
        })
    }

    // Expected behavior:
    // In case of an okay result, the service_handler returns on its own. Before
    // returning, the global shutdown token must be triggered and the shutdown is notified.
    #[test]
    fn service_handler_restart_policy_on_error_service_returns_okay() {
        let mut config_mock = MockConfigFormat::new();
        config_mock
            .expect_try_load()
            .times(1)
            .returning(|_| Ok(String::new()));
        let config = ServiceConfig::try_load(&PathBuf::new(), config_mock).unwrap();

        block_on(async {
            let mut service_mock = MockService::new();
            service_mock
                .expect_run()
                .times(1)
                .returning(|_| Box::pin(ready(Ok(()))));

            let mut integration_mock = MockIntegration::new();
            integration_mock
                .expect_notify_ready()
                .times(1)
                .returning(|| ());
            integration_mock
                .expect_notify_stopping()
                .times(1)
                .returning(|| ());

            let daemon = Daemon::new(integration_mock);
            assert!(
                daemon
                    .service_handler(&service_mock, &config, RestartPolicy::OnError)
                    .timeout(Duration::from_millis(100))
                    .await
                    .is_some()
            );
            assert!(daemon.shutdown.is_canceled());
        })
    }

    // Expected behavior:
    // A service is restarted in case the result was an error, service_handler
    // does not return.
    #[test]
    fn service_handler_restart_policy_on_errpr_service_returns_error() {
        let mut config_mock = MockConfigFormat::new();
        config_mock
            .expect_try_load()
            .times(1)
            .returning(|_| Ok(String::new()));
        let config = ServiceConfig::try_load(&PathBuf::new(), config_mock).unwrap();

        block_on(async {
            let mut service_mock = MockService::new();
            service_mock
                .expect_run()
                .times(2)
                .returning(|_| Box::pin(ready(Err(MockError))));
            service_mock
                .expect_run()
                .times(1)
                .returning(|_| Box::pin(pending()));

            let mut integration_mock = MockIntegration::new();
            integration_mock
                .expect_notify_ready()
                .times(3)
                .returning(|| ());

            let daemon = Daemon::new(integration_mock);
            assert!(
                daemon
                    .service_handler(&service_mock, &config, RestartPolicy::OnError)
                    .timeout(Duration::from_millis(100))
                    .await
                    .is_none()
            );
            assert!(!daemon.shutdown.is_canceled());
        });
    }

    // Expected behavior:
    // The service handler shall return after the daemons cancellation token was triggered.
    #[test]
    fn service_handler_shutdown() {
        block_on(async {
            let mut integration_mock = MockIntegration::new();
            integration_mock
                .expect_notify_ready()
                .times(1)
                .returning(|| ());

            let mut config_loading_mock = MockConfigFormat::new();
            config_loading_mock
                .expect_try_load()
                .returning(|_| Ok(String::default()));

            let mut service_mock = MockService::new();
            service_mock
                .expect_run()
                .times(1)
                .returning(|_| Box::pin(pending()));

            let daemon = Daemon::new(integration_mock);
            let config_mock =
                ServiceConfig::try_load(&PathBuf::new(), config_loading_mock).unwrap();

            join!(
                daemon.service_handler(&service_mock, &config_mock, RestartPolicy::Always),
                async {
                    daemon.shutdown.cancel();
                }
            );
        });
    }

    // Expected behavior:
    // The service handler shall be re-run in case the configuration was updated.
    #[test]
    fn service_handler_restart_on_config_update() {
        block_on(async {
            let mut integration_mock = MockIntegration::new();
            integration_mock
                .expect_notify_ready()
                .times(3)
                .returning(|| ());

            let config1 = "Config 1";
            let config2 = "Config 2";
            let config3 = "Config 3";
            let mut config_mock = MockConfigFormat::new();
            config_mock
                .expect_try_load()
                .returning(|_| Ok(config1.to_string()));

            let mut sequence = Sequence::new();
            let mut service_mock = MockService::new();
            service_mock
                .expect_run()
                .times(1)
                .with(eq(config1.to_string()))
                .in_sequence(&mut sequence)
                .returning(|_| Box::pin(pending()));
            service_mock
                .expect_run()
                .times(1)
                .with(eq(config2.to_string()))
                .in_sequence(&mut sequence)
                .returning(|_| Box::pin(pending()));
            service_mock
                .expect_run()
                .times(1)
                .with(eq(config3.to_string()))
                .in_sequence(&mut sequence)
                .returning(|_| Box::pin(pending()));

            // Re-configure the service multiple times then wait until the mock expectation are
            // fulfilled.
            let daemon = Daemon::new(integration_mock);
            let config_mock = ServiceConfig::try_load(&PathBuf::new(), config_mock).unwrap();
            join!(
                daemon.service_handler(&service_mock, &config_mock, RestartPolicy::Always),
                async {
                    let mut delay = Interval::platform_new(Duration::from_millis(50));
                    delay.as_mut().await;
                    config_mock.sender().send(config2.to_string()).unwrap();
                    delay.as_mut().await;
                    config_mock.sender().send(config3.to_string()).unwrap();
                    delay.as_mut().await;
                    daemon.shutdown.cancel();
                }
            );
        });
    }
}
