use crate::{
  inboxes::RealCommunityInboxCollector,
  send::{SendActivityResult, SendRetryTask, SendSuccessInfo},
  util::{
    get_activity_cached,
    get_latest_activity_id,
    FederationQueueStateWithDomain,
    WORK_FINISHED_RECHECK_DELAY,
  },
};
use activitypub_federation::config::FederationConfig;
use anyhow::{Context, Result};
use chrono::{DateTime, Days, TimeZone, Utc};
use lemmy_api_common::{
  context::LemmyContext,
  federate_retry_sleep_duration,
  lemmy_utils::settings::structs::FederationWorkerConfig,
};
use lemmy_db_schema::{
  newtypes::ActivityId,
  source::{
    federation_queue_state::FederationQueueState,
    instance::{Instance, InstanceForm},
  },
  utils::{ActualDbPool, DbPool},
};
use lemmy_utils::error::LemmyResult;
use std::{collections::BinaryHeap, ops::Add, time::Duration};
use tokio::{
  sync::mpsc::{self, UnboundedSender},
  time::sleep,
};
use tokio_util::sync::CancellationToken;

/// Save state to db after this time has passed since the last state (so if the server crashes or is
/// SIGKILLed, less than X seconds of activities are resent)
#[cfg(not(test))]
static SAVE_STATE_EVERY_TIME: Duration = Duration::from_secs(60);
#[cfg(test)]
/// in test mode, we want it to save state and send it to print_stats after every send
static SAVE_STATE_EVERY_TIME: Duration = Duration::from_secs(0);
/// Maximum number of successful sends to allow out of order
const MAX_SUCCESSFULS: usize = 1000;

/// in prod mode, try to collect multiple send results at the same time to reduce load
#[cfg(not(test))]
const MIN_ACTIVITY_SEND_RESULTS_TO_HANDLE: usize = 4;
#[cfg(test)]
const MIN_ACTIVITY_SEND_RESULTS_TO_HANDLE: usize = 0;

///
/// SendManager --(has many)--> InstanceWorker --(has many)--> SendRetryTask
///      |                            |                               |
/// -----|------create worker -> loop activities--create task-> send activity
///      |                            |                             vvvv
///      |                            |                           fail or success
///      |                            |           <-report result--   |
///      |               <---order and aggrate results---             |
///      |   <---send stats---        |                               |
/// filter and print stats            |                               |
pub(crate) struct InstanceWorker {
  instance: Instance,
  stop: CancellationToken,
  federation_lib_config: FederationConfig<LemmyContext>,
  federation_worker_config: FederationWorkerConfig,
  state: FederationQueueState,
  last_state_insert: DateTime<Utc>,
  pool: ActualDbPool,
  inbox_collector: RealCommunityInboxCollector,
  // regularily send stats back to the SendManager
  stats_sender: UnboundedSender<FederationQueueStateWithDomain>,
  // each HTTP send will report back to this channel concurrently
  receive_send_result: mpsc::UnboundedReceiver<SendActivityResult>,
  // this part of the channel is cloned and passed to the SendRetryTasks
  report_send_result: mpsc::UnboundedSender<SendActivityResult>,
  // activities that have been successfully sent but
  // that are not the lowest number and thus can't be written to the database yet
  successfuls: BinaryHeap<SendSuccessInfo>,
  // number of activities that currently have a task spawned to send it
  in_flight: i8,
}

impl InstanceWorker {
  pub(crate) async fn init_and_loop(
    instance: Instance,
    config: FederationConfig<LemmyContext>,
    federation_worker_config: FederationWorkerConfig,
    stop: CancellationToken,
    stats_sender: UnboundedSender<FederationQueueStateWithDomain>,
  ) -> LemmyResult<()> {
    let pool = config.to_request_data().inner_pool().clone();
    let state = FederationQueueState::load(&mut DbPool::Pool(&pool), instance.id).await?;
    let (report_send_result, receive_send_result) =
      tokio::sync::mpsc::unbounded_channel::<SendActivityResult>();
    let mut worker = InstanceWorker {
      inbox_collector: RealCommunityInboxCollector::new_real(
        pool.clone(),
        instance.id,
        instance.domain.clone(),
      ),
      federation_worker_config,
      instance,
      stop,
      federation_lib_config: config,
      stats_sender,
      state,
      last_state_insert: Utc.timestamp_nanos(0),
      pool,
      receive_send_result,
      report_send_result,
      successfuls: BinaryHeap::<SendSuccessInfo>::new(),
      in_flight: 0,
    };

    worker.loop_until_stopped().await
  }
  /// loop fetch new activities from db and send them to the inboxes of the given instances
  /// this worker only returns if (a) there is an internal error or (b) the cancellation token is
  /// cancelled (graceful exit)
  async fn loop_until_stopped(&mut self) -> LemmyResult<()> {
    self.initial_fail_sleep().await?;
    let (mut last_sent_id, mut newest_id) = self.get_latest_ids().await?;

    while !self.stop.is_cancelled() {
      // check if we need to wait for a send to finish before sending the next one
      // we wait if (a) the last request failed, only if a request is already in flight (not at the
      // start of the loop) or (b) if we have too many successfuls in memory or (c) if we have
      // too many in flight
      let need_wait_for_event = (self.in_flight != 0 && self.state.fail_count > 0)
        || self.successfuls.len() >= MAX_SUCCESSFULS
        || self.in_flight >= self.federation_worker_config.concurrent_sends_per_instance;
      if need_wait_for_event || self.receive_send_result.len() > MIN_ACTIVITY_SEND_RESULTS_TO_HANDLE
      {
        // if len() > 0 then this does not block and allows us to write to db more often
        // if len is 0 then this means we wait for something to change our above conditions,
        // which can only happen by an event sent into the channel
        self.handle_send_results().await?;
        // handle_send_results does not guarantee that we are now in a condition where we want to
        // send a new one, so repeat this check until the if no longer applies
        continue;
      }

      // send a new activity if there is one
      self.inbox_collector.update_communities().await?;
      let next_id_to_send = ActivityId(last_sent_id.0 + 1);
      {
        // sanity check: calculate next id to send based on the last id and the in flight requests
        let expected_next_id = self.state.last_successful_id.map(|last_successful_id| {
          last_successful_id.0 + (self.successfuls.len() as i64) + i64::from(self.in_flight) + 1
        });
        // compare to next id based on incrementing
        if expected_next_id != Some(next_id_to_send.0) {
          return Err(
            anyhow::anyhow!(
              "{}: next id to send is not as expected: {:?} != {:?}",
              self.instance.domain,
              expected_next_id,
              next_id_to_send
            )
            .into(),
          );
        }
      }

      if next_id_to_send > newest_id {
        // lazily fetch latest id only if we have cought up
        newest_id = self.get_latest_ids().await?.1;
        if next_id_to_send > newest_id {
          if next_id_to_send > ActivityId(newest_id.0 + 1) {
            tracing::error!(
                "{}: next send id {} is higher than latest id {}+1 in database (did the db get cleared?)",
                self.instance.domain,
                next_id_to_send.0,
                newest_id.0
              );
          }
          // no more work to be done, wait before rechecking
          tokio::select! {
            () = sleep(*WORK_FINISHED_RECHECK_DELAY) => {},
            () = self.stop.cancelled() => {
              tracing::debug!("cancelled worker loop while waiting for new work")
            }
          }
          continue;
        }
      }
      self.in_flight += 1;
      last_sent_id = next_id_to_send;
      self.spawn_send_if_needed(next_id_to_send).await?;
    }
    tracing::debug!("cancelled worker loop after send");

    // final update of state in db on shutdown
    self.save_and_send_state().await?;
    Ok(())
  }

  async fn initial_fail_sleep(&mut self) -> Result<()> {
    // before starting queue, sleep remaining duration if last request failed
    if self.state.fail_count > 0 {
      let last_retry = self
        .state
        .last_retry
        .context("impossible: if fail count set last retry also set")?;
      let elapsed = (Utc::now() - last_retry).to_std()?;
      let required = federate_retry_sleep_duration(self.state.fail_count);
      if elapsed >= required {
        return Ok(());
      }
      let remaining = required - elapsed;
      tracing::debug!(
        "{}: fail-sleeping for {:?} before starting queue",
        self.instance.domain,
        remaining
      );
      tokio::select! {
        () = sleep(remaining) => {},
        () = self.stop.cancelled() => {
          tracing::debug!("cancelled worker loop during initial fail sleep")
        }
      }
    }
    Ok(())
  }

  /// return the last successfully sent id and the newest activity id in the database
  /// sets last_successful_id in database if it's the first time this instance is seen
  async fn get_latest_ids(&mut self) -> Result<(ActivityId, ActivityId)> {
    let latest_id = get_latest_activity_id(&mut self.pool()).await?;
    let last = if let Some(last) = self.state.last_successful_id {
      last
    } else {
      // this is the initial creation (instance first seen) of the federation queue for this
      // instance

      // skip all past activities:
      self.state.last_successful_id = Some(latest_id);
      // save here to ensure it's not read as 0 again later if no activities have happened
      self.save_and_send_state().await?;
      latest_id
    };
    Ok((last, latest_id))
  }

  async fn handle_send_results(&mut self) -> Result<(), anyhow::Error> {
    let mut force_write = false;
    let mut events = Vec::new();
    // Wait for at least one event but if there's multiple handle them all.
    // We need to listen to the cancel event here as well in order to prevent a hang on shutdown:
    // If the SendRetryTask gets cancelled, it immediately exits without reporting any state.
    // So if the worker is waiting for a send result and all SendRetryTask gets cancelled, this recv
    // could hang indefinitely otherwise. The tasks will also drop their handle of
    // report_send_result which would cause the recv_many method to return 0 elements, but since
    // InstanceWorker holds a copy of the send result channel as well, that won't happen.
    tokio::select! {
      _ = self.receive_send_result.recv_many(&mut events, 1000) => {},
      () = self.stop.cancelled() => {
        tracing::debug!("cancelled worker loop while waiting for send results");
        return Ok(());
      }
    }
    for event in events {
      match event {
        SendActivityResult::Success(s) => {
          self.in_flight -= 1;
          if !s.was_skipped {
            self.state.fail_count = 0;
            self.mark_instance_alive().await?;
          }
          self.successfuls.push(s);
        }
        SendActivityResult::Failure { fail_count, .. } => {
          if fail_count > self.state.fail_count {
            // override fail count - if multiple activities are currently sending this value may get
            // conflicting info but that's fine.
            // This needs to be this way, all alternatives would be worse. The reason is that if 10
            // simultaneous requests fail within a 1s period, we don't want the next retry to be
            // exponentially 2**10 s later. Any amount of failures within a fail-sleep period should
            // only count as one failure.

            self.state.fail_count = fail_count;
            self.state.last_retry = Some(Utc::now());
            force_write = true;
          }
        }
      }
    }
    self.pop_successfuls_and_write(force_write).await?;
    Ok(())
  }
  async fn mark_instance_alive(&mut self) -> Result<()> {
    // Activity send successful, mark instance as alive if it hasn't been updated in a while.
    let updated = self.instance.updated.unwrap_or(self.instance.published);
    if updated.add(Days::new(1)) < Utc::now() {
      self.instance.updated = Some(Utc::now());

      let form = InstanceForm {
        updated: Some(Utc::now()),
        ..InstanceForm::new(self.instance.domain.clone())
      };
      Instance::update(&mut self.pool(), self.instance.id, form).await?;
    }
    Ok(())
  }
  /// Checks that sequential activities `last_successful_id + 1`, `last_successful_id + 2` etc have
  /// been sent successfully. In that case updates `last_successful_id` and saves the state to the
  /// database if the time since the last save is greater than `SAVE_STATE_EVERY_TIME`.
  async fn pop_successfuls_and_write(&mut self, force_write: bool) -> Result<()> {
    let Some(mut last_id) = self.state.last_successful_id else {
      tracing::warn!(
        "{} should be impossible: last successful id is None",
        self.instance.domain
      );
      return Ok(());
    };
    tracing::debug!(
      "{} last: {:?}, next: {:?}, currently in successfuls: {:?}",
      self.instance.domain,
      last_id,
      self.successfuls.peek(),
      self.successfuls.iter()
    );
    while self
      .successfuls
      .peek()
      .map(|a| a.activity_id == ActivityId(last_id.0 + 1))
      .unwrap_or(false)
    {
      let next = self
        .successfuls
        .pop()
        .context("peek above ensures pop has value")?;
      last_id = next.activity_id;
      self.state.last_successful_id = Some(next.activity_id);
      self.state.last_successful_published_time = next.published;
    }

    let save_state_every = chrono::Duration::from_std(SAVE_STATE_EVERY_TIME)?;
    if force_write || (Utc::now() - self.last_state_insert) > save_state_every {
      self.save_and_send_state().await?;
    }
    Ok(())
  }

  /// we collect the relevant inboxes in the main instance worker task, and only spawn the send task
  /// if we have inboxes to send to this limits CPU usage and reduces overhead for the (many)
  /// cases where we don't have any inboxes
  async fn spawn_send_if_needed(&mut self, activity_id: ActivityId) -> LemmyResult<()> {
    let Some(ele) = get_activity_cached(&mut self.pool(), activity_id)
      .await
      .context("failed reading activity from db")?
    else {
      tracing::debug!("{}: {:?} does not exist", self.instance.domain, activity_id);
      self
        .report_send_result
        .send(SendActivityResult::Success(SendSuccessInfo {
          activity_id,
          published: None,
          was_skipped: true,
        }))?;
      return Ok(());
    };
    let activity = &ele.0;
    let inbox_urls = self.inbox_collector.get_inbox_urls(activity).await?;
    if inbox_urls.is_empty() {
      // this is the case when the activity is not relevant to this receiving instance (e.g. no user
      // subscribed to the relevant community)
      tracing::debug!("{}: {:?} no inboxes", self.instance.domain, activity.id);
      self
        .report_send_result
        .send(SendActivityResult::Success(SendSuccessInfo {
          activity_id,
          // it would be valid here to either return None or Some(activity.published). The published
          // time is only used for stats pages that track federation delay. None can be a bit
          // misleading because if you look at / chart the published time for federation from a
          // large to a small instance that's only subscribed to a few small communities,
          // then it will show the last published time as a days ago even though
          // federation is up to date.
          published: Some(activity.published),
          was_skipped: true,
        }))?;
      return Ok(());
    }
    let initial_fail_count = self.state.fail_count;
    let data = self.federation_lib_config.to_request_data();
    let stop = self.stop.clone();
    let domain = self.instance.domain.clone();
    let mut report = self.report_send_result.clone();
    tokio::spawn(async move {
      let res = SendRetryTask {
        activity: &ele.0,
        object: &ele.1,
        inbox_urls,
        report: &mut report,
        initial_fail_count,
        domain,
        context: data,
        stop,
      }
      .send_retry_loop()
      .await;
      if let Err(e) = res {
        tracing::warn!(
          "sending {} errored internally, skipping activity: {:?}",
          ele.0.ap_id,
          e
        );
        // An error in this location means there is some deeper internal issue with the activity,
        // for example the actor can't be loaded or similar. These issues are probably not
        // solveable by retrying and would cause the federation for this instance to permanently be
        // stuck in a retry loop. So we log the error and skip the activity (by reporting success to
        // the worker)
        report
          .send(SendActivityResult::Success(SendSuccessInfo {
            activity_id,
            published: None,
            was_skipped: true,
          }))
          .ok();
      }
    });
    Ok(())
  }

  async fn save_and_send_state(&mut self) -> Result<()> {
    tracing::debug!("{}: saving and sending state", self.instance.domain);
    self.last_state_insert = Utc::now();
    FederationQueueState::upsert(&mut self.pool(), &self.state).await?;
    self.stats_sender.send(FederationQueueStateWithDomain {
      state: self.state.clone(),
      domain: self.instance.domain.clone(),
    })?;
    Ok(())
  }

  fn pool(&self) -> DbPool<'_> {
    DbPool::Pool(&self.pool)
  }
}

#[cfg(test)]
#[expect(clippy::unwrap_used)]
#[expect(clippy::indexing_slicing)]
mod test {

  use super::*;
  use activitypub_federation::{
    http_signatures::generate_actor_keypair,
    protocol::context::WithContext,
  };
  use actix_web::{dev::ServerHandle, web, App, HttpResponse, HttpServer};
  use lemmy_api_common::utils::generate_inbox_url;
  use lemmy_db_schema::{
    newtypes::DbUrl,
    source::{
      activity::{ActorType, SentActivity, SentActivityForm},
      person::{Person, PersonInsertForm},
    },
    traits::Crud,
  };
  use lemmy_utils::error::LemmyResult;
  use serde_json::{json, Value};
  use serial_test::serial;
  use test_context::{test_context, AsyncTestContext};
  use tokio::{
    spawn,
    sync::mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver},
  };
  use tracing_test::traced_test;
  use url::Url;

  struct Data {
    context: activitypub_federation::config::Data<LemmyContext>,
    instance: Instance,
    person: Person,
    stats_receiver: UnboundedReceiver<FederationQueueStateWithDomain>,
    inbox_receiver: UnboundedReceiver<String>,
    cancel: CancellationToken,
    cleaned_up: bool,
    wait_stop_server: ServerHandle,
    is_concurrent: bool,
  }

  impl Data {
    async fn init() -> LemmyResult<Self> {
      let context = LemmyContext::init_test_federation_config().await;
      let instance = Instance::read_or_create(&mut context.pool(), "localhost".to_string()).await?;

      let actor_keypair = generate_actor_keypair()?;
      let ap_id: DbUrl = Url::parse("http://local.com/u/alice")?.into();
      let person_form = PersonInsertForm {
        ap_id: Some(ap_id.clone()),
        private_key: (Some(actor_keypair.private_key)),
        inbox_url: Some(generate_inbox_url()?),
        ..PersonInsertForm::new("alice".to_string(), actor_keypair.public_key, instance.id)
      };
      let person = Person::create(&mut context.pool(), &person_form).await?;

      let cancel = CancellationToken::new();
      let (stats_sender, stats_receiver) = unbounded_channel();
      let (inbox_sender, inbox_receiver) = unbounded_channel();

      // listen for received activities in background
      let wait_stop_server = listen_activities(inbox_sender)?;

      let concurrent_sends_per_instance = std::env::var("LEMMY_TEST_FEDERATION_CONCURRENT_SENDS")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(10);

      let fed_config = FederationWorkerConfig {
        concurrent_sends_per_instance,
      };
      spawn(InstanceWorker::init_and_loop(
        instance.clone(),
        context.clone(),
        fed_config,
        cancel.clone(),
        stats_sender,
      ));
      // wait for startup
      sleep(*WORK_FINISHED_RECHECK_DELAY).await;

      Ok(Self {
        context: context.to_request_data(),
        instance,
        person,
        stats_receiver,
        inbox_receiver,
        cancel,
        wait_stop_server,
        cleaned_up: false,
        is_concurrent: concurrent_sends_per_instance > 1,
      })
    }

    async fn cleanup(&mut self) -> LemmyResult<()> {
      if self.cleaned_up {
        return Ok(());
      }
      self.cleaned_up = true;
      self.cancel.cancel();
      sleep(*WORK_FINISHED_RECHECK_DELAY).await;
      Instance::delete_all(&mut self.context.pool()).await?;
      Person::delete(&mut self.context.pool(), self.person.id).await?;
      self.wait_stop_server.stop(true).await;
      Ok(())
    }
  }

  /// In order to guarantee that the webserver is stopped via the cleanup function,
  /// we implement a test context.
  impl AsyncTestContext for Data {
    async fn setup() -> Data {
      Data::init().await.unwrap()
    }
    async fn teardown(mut self) {
      self.cleanup().await.unwrap()
    }
  }

  #[test_context(Data)]
  #[tokio::test]
  #[traced_test]
  #[serial]
  async fn test_stats(data: &mut Data) -> LemmyResult<()> {
    tracing::debug!("hello world");

    // first receive at startup
    let rcv = data.stats_receiver.recv().await.unwrap();
    tracing::debug!("received first stats");
    assert_eq!(data.instance.id, rcv.state.instance_id);

    let sent = send_activity(data.person.ap_id.clone(), &data.context, true).await?;
    tracing::debug!("sent activity");
    // receive for successfully sent activity
    let inbox_rcv = data.inbox_receiver.recv().await.unwrap();
    let parsed_activity = serde_json::from_str::<WithContext<Value>>(&inbox_rcv)?;
    assert_eq!(&sent.data, parsed_activity.inner());
    tracing::debug!("received activity");

    let rcv = data.stats_receiver.recv().await.unwrap();
    assert_eq!(data.instance.id, rcv.state.instance_id);
    assert_eq!(Some(sent.id), rcv.state.last_successful_id);
    tracing::debug!("received second stats");

    data.cleanup().await?;

    // it also sends state on shutdown
    let rcv = data.stats_receiver.try_recv();
    assert!(rcv.is_ok());

    // nothing further received
    let rcv = data.stats_receiver.try_recv();
    assert_eq!(Some(TryRecvError::Disconnected), rcv.err());
    let inbox_rcv = data.inbox_receiver.try_recv();
    assert_eq!(Some(TryRecvError::Disconnected), inbox_rcv.err());

    Ok(())
  }

  #[test_context(Data)]
  #[tokio::test]
  #[traced_test]
  #[serial]
  async fn test_send_40(data: &mut Data) -> LemmyResult<()> {
    tracing::debug!("hello world");

    // first receive at startup
    let rcv = data.stats_receiver.recv().await.unwrap();
    tracing::debug!("received first stats");
    assert_eq!(data.instance.id, rcv.state.instance_id);
    // assert_eq!(Some(ActivityId(0)), rcv.state.last_successful_id);
    // let last_id_before = rcv.state.last_successful_id.unwrap();
    let mut sent = Vec::new();
    for _ in 0..40 {
      sent.push(send_activity(data.person.ap_id.clone(), &data.context, false).await?);
    }
    sleep(2 * *WORK_FINISHED_RECHECK_DELAY).await;
    tracing::debug!("sent activity");
    compare_sent_with_receive(data, sent).await?;

    Ok(())
  }

  #[test_context(Data)]
  #[tokio::test]
  #[traced_test]
  #[serial]
  /// this test sends 15 activities, waits and checks they have all been received, then sends 50,
  /// etc
  async fn test_send_15_20_30(data: &mut Data) -> LemmyResult<()> {
    tracing::debug!("hello world");

    // first receive at startup
    let rcv = data.stats_receiver.recv().await.unwrap();
    tracing::debug!("received first stats");
    assert_eq!(data.instance.id, rcv.state.instance_id);
    // assert_eq!(Some(ActivityId(0)), rcv.state.last_successful_id);
    // let last_id_before = rcv.state.last_successful_id.unwrap();
    let counts = vec![15, 20, 35];
    for count in counts {
      tracing::debug!("sending {} activities", count);
      let mut sent = Vec::new();
      for _ in 0..count {
        sent.push(send_activity(data.person.ap_id.clone(), &data.context, false).await?);
      }
      sleep(2 * *WORK_FINISHED_RECHECK_DELAY).await;
      tracing::debug!("sent activity");
      compare_sent_with_receive(data, sent).await?;
    }

    Ok(())
  }

  #[test_context(Data)]
  #[tokio::test]
  #[serial]
  async fn test_update_instance(data: &mut Data) -> LemmyResult<()> {
    let form = InstanceForm::new(data.instance.domain.clone());
    Instance::update(&mut data.context.pool(), data.instance.id, form).await?;

    send_activity(data.person.ap_id.clone(), &data.context, true).await?;
    data.inbox_receiver.recv().await.unwrap();

    let instance =
      Instance::read_or_create(&mut data.context.pool(), data.instance.domain.clone()).await?;

    assert!(instance.updated.is_some());

    data.cleanup().await?;

    Ok(())
  }

  fn listen_activities(inbox_sender: UnboundedSender<String>) -> LemmyResult<ServerHandle> {
    let run = HttpServer::new(move || {
      App::new()
        .app_data(actix_web::web::Data::new(inbox_sender.clone()))
        .route(
          "/inbox",
          web::post().to(
            |inbox_sender: actix_web::web::Data<UnboundedSender<String>>, body: String| async move {
              tracing::debug!("received activity: {:?}", body);
              inbox_sender.send(body.clone()).unwrap();
              HttpResponse::new(actix_web::http::StatusCode::OK)
            },
          ),
        )
    })
    .bind(("127.0.0.1", 8085))?
    .run();
    let handle = run.handle();
    tokio::spawn(async move {
      run.await.unwrap();
      /*select! {
        _ = run => {},
        _ = cancel.cancelled() => { }
      }*/
    });
    Ok(handle)
  }

  async fn send_activity(
    ap_id: DbUrl,
    context: &LemmyContext,
    wait: bool,
  ) -> LemmyResult<SentActivity> {
    // create outgoing activity
    let data = json!({
      "actor": "http://ds9.lemmy.ml/u/lemmy_alpha",
      "object": "http://ds9.lemmy.ml/comment/1",
      "type": "Like",
      "id": format!("http://ds9.lemmy.ml/activities/like/{}", uuid::Uuid::new_v4()),
    });
    let form = SentActivityForm {
      ap_id: Url::parse(&format!(
        "http://local.com/activity/{}",
        uuid::Uuid::new_v4()
      ))?
      .into(),
      data,
      sensitive: false,
      send_inboxes: vec![Some(Url::parse("http://localhost:8085/inbox")?.into())],
      send_all_instances: false,
      send_community_followers_of: None,
      actor_type: ActorType::Person,
      actor_apub_id: ap_id,
    };
    let sent = SentActivity::create(&mut context.pool(), form).await?;

    if wait {
      sleep(*WORK_FINISHED_RECHECK_DELAY * 2).await;
    }

    Ok(sent)
  }
  async fn compare_sent_with_receive(data: &mut Data, mut sent: Vec<SentActivity>) -> Result<()> {
    let check_order = !data.is_concurrent; // allow out-of order receiving when running parallel
    let mut received = Vec::new();
    for _ in 0..sent.len() {
      let inbox_rcv = data.inbox_receiver.recv().await.unwrap();
      let parsed_activity = serde_json::from_str::<WithContext<Value>>(&inbox_rcv)?;
      received.push(parsed_activity);
    }
    if !check_order {
      // sort by id
      received.sort_by(|a, b| {
        a.inner()["id"]
          .as_str()
          .unwrap()
          .cmp(b.inner()["id"].as_str().unwrap())
      });
      sent.sort_by(|a, b| {
        a.data["id"]
          .as_str()
          .unwrap()
          .cmp(b.data["id"].as_str().unwrap())
      });
    }
    // receive for successfully sent activity
    for i in 0..sent.len() {
      let sent_activity = &sent[i];
      let received_activity = received[i].inner();
      assert_eq!(&sent_activity.data, received_activity);
      tracing::debug!("received activity");
    }
    Ok(())
  }
}
