use core::str;
use std::{
    collections::{HashMap, HashSet},
    io::Stdin,
    sync::{Arc, Mutex},
    time::Instant,
};

use anyhow::{anyhow, bail, Context, Result};
use auth_git2::GitAuthenticator;
use client::{get_events_from_cache, get_state_from_cache, send_events, sign_event, STATE_KIND};
use console::Term;
use git::{sha1_to_oid, RepoActions};
use git2::{Oid, Repository};
use git_events::{
    generate_cover_letter_and_patch_events, generate_patch_event, get_commit_id_from_patch,
};
use gnostr::{
    client::{self, get_event_from_cache_by_id},
    git::{
        self,
        nostr_url::{CloneUrl, NostrUrlDecoded},
        oid_to_shorthand_string,
    },
    git_events::{self, get_event_root},
    login::{self, get_curent_user},
    repo_ref, repo_state,
};
use nostr_0_34_1::{
    hashes::sha1::Hash as Sha1Hash, nips::nip10::Marker, Event, EventBuilder, EventId, Kind,
    PublicKey, Tag,
};
use nostr_signer_0_34_0::NostrSigner;
use repo_ref::RepoRef;
use repo_state::RepoState;

use crate::{
    client::Client,
    git::Repo,
    list::list_from_remotes,
    utils::{
        count_lines_per_msg_vec, find_proposal_and_patches_by_branch_name, get_all_proposals,
        get_remote_name_by_url, get_short_git_server_name, get_write_protocols_to_try,
        join_with_and, push_error_is_not_authentication_failure, read_line,
        set_protocol_preference, Direction,
    },
};

#[allow(clippy::too_many_lines)]
pub async fn run_push(
    git_repo: &Repo,
    repo_ref: &RepoRef,
    decoded_nostr_url: &NostrUrlDecoded,
    stdin: &Stdin,
    initial_refspec: &str,
    client: &Client,
    list_outputs: Option<HashMap<String, HashMap<String, String>>>,
) -> Result<()> {
    let refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?;

    let proposal_refspecs = refspecs
        .iter()
        .filter(|r| r.contains("refs/heads/pr/"))
        .cloned()
        .collect::<Vec<String>>();

    let mut git_server_refspecs = refspecs
        .iter()
        .filter(|r| !r.contains("refs/heads/pr/"))
        .cloned()
        .collect::<Vec<String>>();

    let term = console::Term::stderr();

    let list_outputs = match list_outputs {
        Some(outputs) => outputs,
        _ => list_from_remotes(&term, git_repo, &repo_ref.git_server, decoded_nostr_url),
    };

    let nostr_state = get_state_from_cache(git_repo.get_path()?, repo_ref).await;

    let existing_state = {
        // if no state events - create from first git server listed
        if let Ok(nostr_state) = &nostr_state {
            nostr_state.state.clone()
        } else if let Some(url) = repo_ref
            .git_server
            .iter()
            .find(|&url| list_outputs.contains_key(url))
        {
            list_outputs.get(url).unwrap().to_owned()
        } else {
            bail!(
                "cannot connect to git servers: {}",
                repo_ref.git_server.join(" ")
            );
        }
    };

    let refspecs_for_nostr_state = git_server_refspecs.clone();

    let (rejected_refspecs, remote_refspecs) = create_rejected_refspecs_and_remotes_refspecs(
        &term,
        git_repo,
        &git_server_refspecs,
        &existing_state,
        &list_outputs,
    )?;

    git_server_refspecs.retain(|refspec| {
        if let Some(rejected) = rejected_refspecs.get(&refspec.clone()) {
            let (_, to) = refspec_to_from_to(refspec).unwrap();
            println!("error {to} {} out of sync with nostr", rejected.join(" "));
            false
        } else {
            true
        }
    });

    let mut events = vec![];

    if git_server_refspecs.is_empty() && proposal_refspecs.is_empty() {
        // all refspecs rejected
        println!();
        return Ok(());
    }

    let (signer, user_ref) = login::launch(
        git_repo,
        &None,
        &None,
        &None,
        &None,
        Some(client),
        false,
        true,
    )
    .await?;

    if !repo_ref.maintainers.contains(&user_ref.public_key) {
        for refspec in &git_server_refspecs {
            let (_, to) = refspec_to_from_to(refspec).unwrap();
            println!(
                "error {to} your nostr account {} isn't listed as a maintainer of the repo",
                user_ref.metadata.name
            );
        }
        git_server_refspecs.clear();
        if proposal_refspecs.is_empty() {
            println!();
            return Ok(());
        }
    }

    if !refspecs_for_nostr_state.is_empty() {
        let new_state = generate_updated_state(git_repo, &existing_state, &refspecs_for_nostr_state)?;

        let new_repo_state =
            RepoState::build(repo_ref.identifier.clone(), new_state, &signer).await?;

        events.push(new_repo_state.event);

        for event in get_merged_status_events(
            &term,
            repo_ref,
            git_repo,
            &decoded_nostr_url.original_string,
            &signer,
            &git_server_refspecs,
        )
        .await?
        {
            events.push(event);
        }
    }

    let mut rejected_proposal_refspecs = vec![];
    if !proposal_refspecs.is_empty() {
        let all_proposals = get_all_proposals(git_repo, repo_ref).await?;
        let current_user = get_curent_user(git_repo)?;

        for refspec in &proposal_refspecs {
            let (from, to) = refspec_to_from_to(refspec).unwrap();
            let tip_of_pushed_branch = git_repo.get_commit_or_tip_of_reference(from)?;

            if let Some((_, (proposal, patches))) =
                find_proposal_and_patches_by_branch_name(to, &all_proposals, current_user.as_ref())
            {
                if [repo_ref.maintainers.clone(), vec![proposal.author()]]
                    .concat()
                    .contains(&user_ref.public_key)
                {
                    if refspec.starts_with('+') {
                        // force push
                        let (_, main_tip) = git_repo.get_main_or_master_branch()?;
                        let (mut ahead, _) =
                            git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?;
                        ahead.reverse();
                        for patch in generate_cover_letter_and_patch_events(
                            None,
                            git_repo,
                            &ahead,
                            &signer,
                            repo_ref,
                            &Some(proposal.id().to_string()),
                            &[],
                        )
                        .await?
                        {
                            events.push(patch);
                        }
                    } else {
                        // fast forward push
                        let tip_patch = patches.first().unwrap();
                        let tip_of_proposal = get_commit_id_from_patch(tip_patch)?;
                        let tip_of_proposal_commit =
                            git_repo.get_commit_or_tip_of_reference(&tip_of_proposal)?;

                        let (mut ahead, behind) = git_repo.get_commits_ahead_behind(
                            &tip_of_proposal_commit,
                            &tip_of_pushed_branch,
                        )?;
                        if behind.is_empty() {
                            let thread_id = if let Ok(root_event_id) = get_event_root(tip_patch) {
                                root_event_id
                            } else {
                                // tip patch is the root proposal
                                tip_patch.id()
                            };
                            let mut parent_patch = tip_patch.clone();
                            ahead.reverse();
                            for (i, commit) in ahead.iter().enumerate() {
                                let new_patch = generate_patch_event(
                                    git_repo,
                                    &git_repo.get_root_commit()?,
                                    commit,
                                    Some(thread_id),
                                    &signer,
                                    repo_ref,
                                    Some(parent_patch.id()),
                                    Some((
                                        (patches.len() + i + 1).try_into().unwrap(),
                                        (patches.len() + ahead.len()).try_into().unwrap(),
                                    )),
                                    None,
                                    &None,
                                    &[],
                                )
                                .await
                                .context("cannot make patch event from commit")?;
                                events.push(new_patch.clone());
                                parent_patch = new_patch;
                            }
                        } else {
                            // we shouldn't get here
                            term.write_line(
                                format!(
                                    "WARNING: failed to push {from} as nostr proposal. Try and force push ",
                                )
                                .as_str(),
                            )
                            .unwrap();
                            println!(
                                "error {to} cannot fastforward as newer patches found on proposal"
                            );
                            rejected_proposal_refspecs.push(refspec.clone());
                        }
                    }
                } else {
                    println!(
						"error {to} permission denied. you are not the proposal author or a repo maintainer"
					);
                    rejected_proposal_refspecs.push(refspec.clone());
                }
            } else {
                // TODO new proposal / couldn't find exisiting
                // proposal
                let (_, main_tip) = git_repo.get_main_or_master_branch()?;
                let (mut ahead, _) =
                    git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?;
                ahead.reverse();
                for patch in generate_cover_letter_and_patch_events(
                    None,
                    git_repo,
                    &ahead,
                    &signer,
                    repo_ref,
                    &None,
                    &[],
                )
                .await?
                {
                    events.push(patch);
                }
            }
        }
    }

    // TODO check whether tip of each branch pushed is on at least one
    // git server before broadcasting the nostr state
    if !events.is_empty() {
        send_events(
            client,
            git_repo.get_path()?,
            events,
            user_ref.relays.write(),
            repo_ref.relays.clone(),
            false,
            true,
        )
        .await?;
    }

    for refspec in &[git_server_refspecs.clone(), proposal_refspecs.clone()].concat() {
        if rejected_proposal_refspecs.contains(refspec) {
            continue;
        }
        let (_, to) = refspec_to_from_to(refspec)?;
        println!("ok {to}");
        update_remote_refs_pushed(
            &git_repo.git_repo,
            refspec,
            &decoded_nostr_url.original_string,
        )
        .context("could not update remote_ref locally")?;
    }

    // TODO make async - check gitlib2 callbacks work async

    for (git_server_url, remote_refspecs) in remote_refspecs {
        let remote_refspecs = remote_refspecs
            .iter()
            .filter(|refspec| git_server_refspecs.contains(refspec))
            .cloned()
            .collect::<Vec<String>>();
        if !refspecs.is_empty() {
            let _ = push_to_remote(
                git_repo,
                &git_server_url,
                decoded_nostr_url,
                &remote_refspecs,
                &term,
            );
        }
    }
    println!();
    Ok(())
}

fn push_to_remote(
    git_repo: &Repo,
    git_server_url: &str,
    decoded_nostr_url: &NostrUrlDecoded,
    remote_refspecs: &[String],
    term: &Term,
) -> Result<()> {
    let server_url = git_server_url.parse::<CloneUrl>()?;
    let protocols_to_attempt = get_write_protocols_to_try(git_repo, &server_url, decoded_nostr_url);

    let mut failed_protocols = vec![];
    let mut success = false;

    for protocol in &protocols_to_attempt {
        term.write_line(format!("push: {} over {protocol}...", server_url.short_name(),).as_str())?;

        let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?;

        if let Err(error) = push_to_remote_url(git_repo, &formatted_url, remote_refspecs, term) {
            term.write_line(
                format!("push: {formatted_url} failed over {protocol}: {error}").as_str(),
            )?;
            failed_protocols.push(protocol);
            if push_error_is_not_authentication_failure(&error) {
                break;
            }
        } else {
            success = true;
            if !failed_protocols.is_empty() {
                term.write_line(format!("push: succeeded over {protocol}").as_str())?;
                let _ = set_protocol_preference(git_repo, protocol, &server_url, &Direction::Push);
            }
            break;
        }
    }
    if success {
        Ok(())
    } else {
        let error = anyhow!(
            "{} failed over {}{}",
            server_url.short_name(),
            join_with_and(&failed_protocols),
            if decoded_nostr_url.protocol.is_some() {
                " and nostr url contains protocol override so no other protocols were attempted"
            } else {
                ""
            },
        );
        term.write_line(format!("push: {error}").as_str())?;
        Err(error)
    }
}

fn push_to_remote_url(
    git_repo: &Repo,
    git_server_url: &str,
    remote_refspecs: &[String],
    term: &Term,
) -> Result<()> {
    let git_config = git_repo.git_repo.config()?;
    let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?;
    let auth = GitAuthenticator::default();
    let mut push_options = git2::PushOptions::new();
    let mut remote_callbacks = git2::RemoteCallbacks::new();
    let push_reporter = Arc::new(Mutex::new(PushReporter::new(term)));

    remote_callbacks.credentials(auth.credentials(&git_config));

    remote_callbacks.push_update_reference({
        let push_reporter = Arc::clone(&push_reporter);
        move |name, error| {
            let mut reporter = push_reporter.lock().unwrap();
            if let Some(error) = error {
                let existing_lines = reporter.count_all_existing_lines();
                reporter.update_reference_errors.push(format!(
                    "WARNING: {} failed to push {name} error: {error}",
                    get_short_git_server_name(git_repo, git_server_url),
                ));
                reporter.write_all(existing_lines);
            }
            Ok(())
        }
    });

    remote_callbacks.push_negotiation({
        let push_reporter = Arc::clone(&push_reporter);
        move |updates| {
            let mut reporter = push_reporter.lock().unwrap();
            let existing_lines = reporter.count_all_existing_lines();

            for update in updates {
                let dst_refname = update
                    .dst_refname()
                    .unwrap_or("")
                    .replace("refs/heads/", "")
                    .replace("refs/tags/", "tags/");
                let msg = if update.dst().is_zero() {
                    format!("push: - [delete]          {dst_refname}")
                } else if update.src().is_zero() {
                    if update.dst_refname().unwrap_or("").contains("refs/tags") {
                        format!("push: * [new tag]         {dst_refname}")
                    } else {
                        format!("push: * [new branch]      {dst_refname}")
                    }
                } else {
                    let force = remote_refspecs
                        .iter()
                        .any(|r| r.contains(&dst_refname) && r.contains('+'));
                    format!(
                        "push: {} {}..{}  {} -> {dst_refname}",
                        if force { "+" } else { " " },
                        oid_to_shorthand_string(update.src()).unwrap(),
                        oid_to_shorthand_string(update.dst()).unwrap(),
                        update
                            .src_refname()
                            .unwrap_or("")
                            .replace("refs/heads/", "")
                            .replace("refs/tags/", "tags/"),
                    )
                };
                // other possibilities will result in push to fail but
                // better reporting is needed:
                // deleting a non-existant branch:
                // ! [remote rejected] <old-branch-name> ->
                // <old-branch-name> (not found) adding a branch
                // that already exists: ! [remote rejected]
                // <new-branch-name> -> <new-branch-name> (already
                // exists) pushing without non-fast-forward
                // without force: ! [rejected]
                // <branch-name> -> <branch-name> (non-fast-forward)
                reporter.negotiation.push(msg);
            }
            reporter.write_all(existing_lines);
            Ok(())
        }
    });

    remote_callbacks.push_transfer_progress({
        let push_reporter = Arc::clone(&push_reporter);
        #[allow(clippy::cast_precision_loss)]
        move |current, total, bytes| {
            let mut reporter = push_reporter.lock().unwrap();
            reporter.process_transfer_progress_update(current, total, bytes);
        }
    });

    remote_callbacks.sideband_progress({
        let push_reporter = Arc::clone(&push_reporter);
        move |data| {
            let mut reporter = push_reporter.lock().unwrap();
            reporter.process_remote_msg(data);
            true
        }
    });
    push_options.remote_callbacks(remote_callbacks);
    git_server_remote.push(remote_refspecs, Some(&mut push_options))?;
    let _ = git_server_remote.disconnect();
    Ok(())
}

#[allow(clippy::cast_precision_loss)]
#[allow(clippy::float_cmp)]
#[allow(clippy::needless_pass_by_value)]
fn report_on_transfer_progress(
    current: usize,
    total: usize,
    bytes: usize,
    start_time: &Instant,
    end_time: Option<&Instant>,
) -> Option<String> {
    if total == 0 {
        return None;
    }
    let percentage = ((current as f64 / total as f64) * 100.0)
        // always round down because 100% complete is misleading when
        // its not complete
        .floor();
    let (size, unit) = if bytes as f64 >= (1024.0 * 1024.0) {
        (bytes as f64 / (1024.0 * 1024.0), "MiB")
    } else {
        (bytes as f64 / 1024.0, "KiB")
    };
    let speed = {
        let duration = if let Some(end_time) = end_time {
            (*end_time - *start_time).as_millis() as f64
        } else {
            start_time.elapsed().as_millis() as f64
        };

        if duration > 0.0 {
            (bytes as f64 / (1024.0 * 1024.0)) / (duration / 1000.0) // Convert bytes to MiB and milliseconds to seconds
        } else {
            0.0
        }
    };

    Some(format!(
		"push: Writing objects: {percentage}% ({current}/{total}) {size:.2} {unit}  | {speed:.2} MiB/s{}",
		if current == total { ", done." } else { "" },
	))
}

struct PushReporter<'a> {
    remote_msgs: Vec<String>,
    negotiation: Vec<String>,
    transfer_progress_msgs: Vec<String>,
    update_reference_errors: Vec<String>,
    term: &'a console::Term,
    start_time: Option<Instant>,
    end_time: Option<Instant>,
}
impl<'a> PushReporter<'a> {
    fn new(term: &'a console::Term) -> Self {
        Self {
            remote_msgs: vec![],
            negotiation: vec![],
            transfer_progress_msgs: vec![],
            update_reference_errors: vec![],
            term,
            start_time: None,
            end_time: None,
        }
    }
    fn write_all(&self, lines_to_clear: usize) {
        let _ = self.term.clear_last_lines(lines_to_clear);
        for msg in &self.remote_msgs {
            let _ = self.term.write_line(format!("remote: {msg}").as_str());
        }
        for msg in &self.negotiation {
            let _ = self.term.write_line(msg);
        }
        for msg in &self.transfer_progress_msgs {
            let _ = self.term.write_line(msg);
        }
        for msg in &self.update_reference_errors {
            let _ = self.term.write_line(msg);
        }
    }

    fn count_all_existing_lines(&self) -> usize {
        let width = self.term.size().1;
        count_lines_per_msg_vec(width, &self.remote_msgs, "remote: ".len())
            + count_lines_per_msg_vec(width, &self.negotiation, 0)
            + count_lines_per_msg_vec(width, &self.transfer_progress_msgs, 0)
            + count_lines_per_msg_vec(width, &self.update_reference_errors, 0)
    }
    fn process_remote_msg(&mut self, data: &[u8]) {
        if let Ok(data) = str::from_utf8(data) {
            let data = data
                .split(['\n', '\r'])
                .map(str::trim)
                .filter(|line| !line.trim().is_empty())
                .collect::<Vec<&str>>();
            for data in data {
                let existing_lines = self.count_all_existing_lines();
                let msg = data.to_string();
                if let Some(last) = self.remote_msgs.last() {
                    if (last.contains('%') && !last.contains("100%"))
                        || last == &msg.replace(", done.", "")
                    {
                        self.remote_msgs.pop();
                        self.remote_msgs.push(msg);
                    } else {
                        self.remote_msgs.push(msg);
                    }
                } else {
                    self.remote_msgs.push(msg);
                }
                self.write_all(existing_lines);
            }
        }
    }
    fn process_transfer_progress_update(&mut self, current: usize, total: usize, bytes: usize) {
        if self.start_time.is_none() {
            self.start_time = Some(Instant::now());
        }
        if let Some(report) = report_on_transfer_progress(
            current,
            total,
            bytes,
            &self.start_time.unwrap(),
            self.end_time.as_ref(),
        ) {
            let existing_lines = self.count_all_existing_lines();
            if report.contains("100%") {
                self.end_time = Some(Instant::now());
            }
            self.transfer_progress_msgs = vec![report];
            self.write_all(existing_lines);
        }
    }
}

type HashMapUrlRefspecs = HashMap<String, Vec<String>>;

#[allow(clippy::too_many_lines)]
fn create_rejected_refspecs_and_remotes_refspecs(
    term: &console::Term,
    git_repo: &Repo,
    refspecs: &Vec<String>,
    nostr_state: &HashMap<String, String>,
    list_outputs: &HashMap<String, HashMap<String, String>>,
) -> Result<(HashMapUrlRefspecs, HashMapUrlRefspecs)> {
    let mut refspecs_for_remotes = HashMap::new();

    let mut rejected_refspecs: HashMapUrlRefspecs = HashMap::new();

    for (url, remote_state) in list_outputs {
        let short_name = get_short_git_server_name(git_repo, url);
        let mut refspecs_for_remote = vec![];
        for refspec in refspecs {
            let (from, to) = refspec_to_from_to(refspec)?;
            let nostr_value = nostr_state.get(to);
            let remote_value = remote_state.get(to);
            if from.is_empty() {
                if remote_value.is_some() {
                    // delete remote branch
                    refspecs_for_remote.push(refspec.clone());
                }
                continue;
            }
            let from_tip = git_repo.get_commit_or_tip_of_reference(from)?;
            if let Some(nostr_value) = nostr_value {
                if let Some(remote_value) = remote_value {
                    if nostr_value.eq(remote_value) {
                        // in sync - existing branch at same state
                        let is_remote_tip_ancestor_of_commit = if let Ok(remote_value_tip) =
                            git_repo.get_commit_or_tip_of_reference(remote_value)
                        {
                            if let Ok((_, behind)) =
                                git_repo.get_commits_ahead_behind(&remote_value_tip, &from_tip)
                            {
                                behind.is_empty()
                            } else {
                                false
                            }
                        } else {
                            false
                        };
                        if is_remote_tip_ancestor_of_commit {
                            refspecs_for_remote.push(refspec.clone());
                        } else {
                            // this is a force push so we need to
                            // force push to git server too
                            if refspec.starts_with('+') {
                                refspecs_for_remote.push(refspec.clone());
                            } else {
                                refspecs_for_remote.push(format!("+{refspec}"));
                            }
                        }
                    } else if let Ok(remote_value_tip) =
                        git_repo.get_commit_or_tip_of_reference(remote_value)
                    {
                        if from_tip.eq(&remote_value_tip) {
                            // remote already at correct state
                            term.write_line(
                                format!("{short_name} {to} already up-to-date").as_str(),
                            )?;
                        }
                        let (ahead_of_local, behind_local) =
                            git_repo.get_commits_ahead_behind(&from_tip, &remote_value_tip)?;
                        if ahead_of_local.is_empty() {
                            // can soft push
                            refspecs_for_remote.push(refspec.clone());
                        } else {
                            // cant soft push
                            let (ahead_of_nostr, behind_nostr) = git_repo
                                .get_commits_ahead_behind(
                                    &git_repo.get_commit_or_tip_of_reference(nostr_value)?,
                                    &remote_value_tip,
                                )?;
                            if ahead_of_nostr.is_empty() {
                                // ancestor of nostr and we are force
                                // pushing anyway...
                                refspecs_for_remote.push(refspec.clone());
                            } else {
                                rejected_refspecs
                                    .entry(refspec.clone())
                                    .and_modify(|a| a.push(url.clone()))
                                    .or_insert(vec![url.clone()]);
                                term.write_line(
                                    format!(
                                        "ERROR: {short_name} {to} conflicts with nostr ({} ahead {} behind) and local ({} ahead {} behind). either:\r\n  1. pull from that git server and resolve\r\n  2. force push your branch to the git server before pushing to nostr remote",
                                        ahead_of_nostr.len(),
                                        behind_nostr.len(),
                                        ahead_of_local.len(),
                                        behind_local.len(),
                                    ).as_str(),
                                )?;
                            }
                        }
                    } else {
                        // remote_value oid is not present locally
                        // TODO can we download the remote reference?

                        // cant soft push
                        rejected_refspecs
                            .entry(refspec.clone())
                            .and_modify(|a| a.push(url.clone()))
                            .or_insert(vec![url.clone()]);
                        term.write_line(
                            format!("ERROR: {short_name} {to} conflicts with nostr and is not an ancestor of local branch. either:\r\n  1. pull from that git server and resolve\r\n  2. force push your branch to the git server before pushing to nostr remote").as_str(),
                        )?;
                    }
                } else {
                    // existing nostr branch not on remote
                    // report - creating new branch
                    term.write_line(
                        format!(
                            "{short_name} {to} doesn't exist and will be added as a new branch"
                        )
                        .as_str(),
                    )?;
                    refspecs_for_remote.push(refspec.clone());
                }
            } else if let Some(remote_value) = remote_value {
                // new to nostr but on remote
                if let Ok(remote_value_tip) = git_repo.get_commit_or_tip_of_reference(remote_value)
                {
                    let (ahead, behind) =
                        git_repo.get_commits_ahead_behind(&from_tip, &remote_value_tip)?;
                    if behind.is_empty() {
                        // can soft push
                        refspecs_for_remote.push(refspec.clone());
                    } else {
                        // cant soft push
                        rejected_refspecs
                            .entry(refspec.clone())
                            .and_modify(|a| a.push(url.clone()))
                            .or_insert(vec![url.clone()]);
                        term.write_line(
                                    format!(
                                        "ERROR: {short_name} already contains {to} {} ahead and {} behind local branch. either:\r\n  1. pull from that git server and resolve\r\n  2. force push your branch to the git server before pushing to nostr remote",
                                        ahead.len(),
                                        behind.len(),
                                    ).as_str(),
                                )?;
                    }
                } else {
                    // havn't fetched oid from remote
                    // TODO fetch oid from remote
                    // cant soft push
                    rejected_refspecs
                        .entry(refspec.clone())
                        .and_modify(|a| a.push(url.clone()))
                        .or_insert(vec![url.clone()]);
                    term.write_line(
                        format!("ERROR: {short_name} already contains {to} at {remote_value} which is not an ancestor of local branch. either:\r\n  1. pull from that git server and resolve\r\n  2. force push your branch to the git server before pushing to nostr remote").as_str(),
                    )?;
                }
            } else {
                // in sync - new branch
                refspecs_for_remote.push(refspec.clone());
            }
        }
        if !refspecs_for_remote.is_empty() {
            refspecs_for_remotes.insert(url.clone(), refspecs_for_remote);
        }
    }

    // remove rejected refspecs so they dont get pushed to some
    // remotes
    let mut remotes_refspecs_without_rejected = HashMap::new();
    for (url, value) in &refspecs_for_remotes {
        remotes_refspecs_without_rejected.insert(
            url.clone(),
            value
                .iter()
                .filter(|refspec| !rejected_refspecs.contains_key(*refspec))
                .cloned()
                .collect(),
        );
    }
    Ok((rejected_refspecs, remotes_refspecs_without_rejected))
}

fn generate_updated_state(
    git_repo: &Repo,
    existing_state: &HashMap<String, String>,
    refspecs: &Vec<String>,
) -> Result<HashMap<String, String>> {
    let mut new_state = existing_state.clone();

    for refspec in refspecs {
        let (from, to) = refspec_to_from_to(refspec)?;
        if from.is_empty() {
            // delete
            new_state.remove(to);
            if to.contains("refs/tags") {
                new_state.remove(&format!("{to}{}", "^{}"));
            }
        } else if to.contains("refs/tags") {
            new_state.insert(
                format!("{to}{}", "^{}"),
                git_repo
                    .get_commit_or_tip_of_reference(from)
                    .unwrap()
                    .to_string(),
            );
            new_state.insert(
                to.to_string(),
                git_repo
                    .git_repo
                    .find_reference(to)
                    .unwrap()
                    .peel(git2::ObjectType::Tag)
                    .unwrap()
                    .id()
                    .to_string(),
            );
        } else {
            // add or update
            new_state.insert(
                to.to_string(),
                git_repo
                    .get_commit_or_tip_of_reference(from)
                    .unwrap()
                    .to_string(),
            );
        }
    }
    Ok(new_state)
}

async fn get_merged_status_events(
    term: &console::Term,
    repo_ref: &RepoRef,
    git_repo: &Repo,
    remote_nostr_url: &str,
    signer: &NostrSigner,
    refspecs_to_git_server: &Vec<String>,
) -> Result<Vec<Event>> {
    let mut events = vec![];
    for refspec in refspecs_to_git_server {
        let (from, to) = refspec_to_from_to(refspec)?;
        if to.eq("refs/heads/main") || to.eq("refs/heads/master") {
            let tip_of_pushed_branch = git_repo.get_commit_or_tip_of_reference(from)?;
            let Ok(tip_of_remote_branch) = git_repo.get_commit_or_tip_of_reference(
                &refspec_remote_ref_name(&git_repo.git_repo, refspec, remote_nostr_url)?,
            ) else {
                // branch not on remote
                continue;
            };
            let (ahead, _) =
                git_repo.get_commits_ahead_behind(&tip_of_remote_branch, &tip_of_pushed_branch)?;
            for commit_hash in ahead {
                let commit = git_repo.git_repo.find_commit(sha1_to_oid(&commit_hash)?)?;
                if commit.parent_count() > 1 {
                    // merge commit
                    for parent in commit.parents() {
                        // lookup parent id
                        let commit_events = get_events_from_cache(
                            git_repo.get_path()?,
                            vec![nostr_0_34_1::Filter::default()
                                .kind(nostr_0_34_1::Kind::GitPatch)
                                .reference(parent.id().to_string())],
                        )
                        .await?;
                        if let Some(commit_event) = commit_events.iter().find(|e| {
                            e.tags.iter().any(|t| {
                                t.as_vec()[0].eq("commit")
                                    && t.as_vec()[1].eq(&parent.id().to_string())
                            })
                        }) {
                            let (proposal_id, revision_id) =
                                get_proposal_and_revision_root_from_patch(git_repo, commit_event)
                                    .await?;
                            term.write_line(
                                format!(
                                    "merge commit {}: create nostr proposal status event",
                                    &commit.id().to_string()[..7],
                                )
                                .as_str(),
                            )?;

                            events.push(
                                create_merge_status(
                                    signer,
                                    repo_ref,
                                    &get_event_from_cache_by_id(git_repo, &proposal_id).await?,
                                    if let Some(revision_id) = revision_id {
                                        Some(
                                            get_event_from_cache_by_id(git_repo, &revision_id)
                                                .await?,
                                        )
                                    } else {
                                        None
                                    }.as_ref(),
                                    &commit_hash,
                                    commit_event.id(),
                                )
                                .await?,
                            );
                        }
                    }
                }
            }
        }
    }
    Ok(events)
}

async fn create_merge_status(
    signer: &NostrSigner,
    repo_ref: &RepoRef,
    proposal: &Event,
    revision: Option<&Event>,
    merge_commit: &Sha1Hash,
    merged_patch: EventId,
) -> Result<Event> {
    let mut public_keys = repo_ref
        .maintainers
        .iter()
        .copied()
        .collect::<HashSet<PublicKey>>();
    public_keys.insert(proposal.author());
    if let Some(revision) = revision {
        public_keys.insert(revision.author());
    }
    sign_event(
        EventBuilder::new(
            Kind::GitStatusApplied,
            String::new(),
            [
                vec![
                    Tag::custom(
                        nostr_0_34_1::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
                        vec!["git proposal merged / applied".to_string()],
                    ),
                    Tag::from_standardized(nostr_0_34_1::TagStandard::Event {
                        event_id: proposal.id(),
                        relay_url: repo_ref.relays.first().map(nostr_0_34_1::UncheckedUrl::new),
                        marker: Some(Marker::Root),
                        public_key: None,
                    }),
                    Tag::from_standardized(nostr_0_34_1::TagStandard::Event {
                        event_id: merged_patch,
                        relay_url: repo_ref.relays.first().map(nostr_0_34_1::UncheckedUrl::new),
                        marker: Some(Marker::Mention),
                        public_key: None,
                    }),
                ],
                if let Some(revision) = revision {
                    vec![Tag::from_standardized(nostr_0_34_1::TagStandard::Event {
                        event_id: revision.id(),
                        relay_url: repo_ref.relays.first().map(nostr_0_34_1::UncheckedUrl::new),
                        marker: Some(Marker::Root),
                        public_key: None,
                    })]
                } else {
                    vec![]
                },
                public_keys.iter().map(|pk| Tag::public_key(*pk)).collect(),
                repo_ref
                    .coordinates()
                    .iter()
                    .map(|c| Tag::coordinate(c.clone()))
                    .collect::<Vec<Tag>>(),
                vec![
                    Tag::from_standardized(nostr_0_34_1::TagStandard::Reference(
                        repo_ref.root_commit.clone(),
                    )),
                    Tag::from_standardized(nostr_0_34_1::TagStandard::Reference(format!(
                        "{merge_commit}"
                    ))),
                    Tag::custom(
                        nostr_0_34_1::TagKind::Custom(std::borrow::Cow::Borrowed(
                            "merge-commit-id",
                        )),
                        vec![format!("{merge_commit}")],
                    ),
                ],
            ]
            .concat(),
        ),
        signer,
    )
    .await
}

async fn get_proposal_and_revision_root_from_patch(
    git_repo: &Repo,
    patch: &Event,
) -> Result<(EventId, Option<EventId>)> {
    let proposal_or_revision = if patch.tags.iter().any(|t| t.as_vec()[1].eq("root")) {
        patch.clone()
    } else {
        let proposal_or_revision_id = EventId::parse(
            if let Some(t) = patch.tags.iter().find(|t| t.is_root()) {
                t.clone()
            } else if let Some(t) = patch.tags.iter().find(|t| t.is_reply()) {
                t.clone()
            } else {
                Tag::event(patch.id())
            }
            .as_vec()[1]
                .clone(),
        )?;

        get_events_from_cache(
            git_repo.get_path()?,
            vec![nostr_0_34_1::Filter::default().id(proposal_or_revision_id)],
        )
        .await?
        .first()
        .unwrap()
        .clone()
    };

    if !proposal_or_revision.kind().eq(&Kind::GitPatch) {
        bail!("thread root is not a git patch");
    }

    if proposal_or_revision
        .tags
        .iter()
        .any(|t| t.as_vec()[1].eq("revision-root"))
    {
        Ok((
            EventId::parse(
                proposal_or_revision
                    .tags
                    .iter()
                    .find(|t| t.is_reply())
                    .unwrap()
                    .as_vec()[1]
                    .clone(),
            )?,
            Some(proposal_or_revision.id()),
        ))
    } else {
        Ok((proposal_or_revision.id(), None))
    }
}

fn update_remote_refs_pushed(
    git_repo: &Repository,
    refspec: &str,
    nostr_remote_url: &str,
) -> Result<()> {
    let (from, _) = refspec_to_from_to(refspec)?;

    let target_ref_name = refspec_remote_ref_name(git_repo, refspec, nostr_remote_url)?;

    if from.is_empty() {
        if let Ok(mut remote_ref) = git_repo.find_reference(&target_ref_name) {
            remote_ref.delete()?;
        }
    } else {
        let commit = reference_to_commit(git_repo, from)
            .context(format!("cannot get commit of reference {from}"))?;
        if let Ok(mut remote_ref) = git_repo.find_reference(&target_ref_name) {
            remote_ref.set_target(commit, "updated by nostr remote helper")?;
        } else {
            git_repo.reference(
                &target_ref_name,
                commit,
                false,
                "created by nostr remote helper",
            )?;
        }
    }
    Ok(())
}

fn refspec_to_from_to(refspec: &str) -> Result<(&str, &str)> {
    if !refspec.contains(':') {
        bail!(
            "refspec should contain a colon (:) but consists of: {}",
            refspec
        );
    }
    let parts = refspec.split(':').collect::<Vec<&str>>();
    Ok((
        if parts.first().unwrap().starts_with('+') {
            &parts.first().unwrap()[1..]
        } else {
            parts.first().unwrap()
        },
        parts.get(1).unwrap(),
    ))
}

fn refspec_remote_ref_name(
    git_repo: &Repository,
    refspec: &str,
    nostr_remote_url: &str,
) -> Result<String> {
    let (_, to) = refspec_to_from_to(refspec)?;
    let nostr_remote = git_repo
        .find_remote(&get_remote_name_by_url(git_repo, nostr_remote_url)?)
        .context("we should have just located this remote")?;
    Ok(format!(
        "refs/remotes/{}/{}",
        nostr_remote.name().context("remote should have a name")?,
        to.replace("refs/heads/", ""), /* TODO only replace if it
                                        * begins with this
                                        * TODO what about tags? */
    ))
}

fn reference_to_commit(git_repo: &Repository, reference: &str) -> Result<Oid> {
    Ok(git_repo
        .find_reference(reference)
        .context(format!("cannot find reference: {reference}"))?
        .peel_to_commit()
        .context(format!("cannot get commit from reference: {reference}"))?
        .id())
}

// this maybe a commit id or a ref: pointer
fn reference_to_ref_value(git_repo: &Repository, reference: &str) -> Result<String> {
    let reference_obj = git_repo
        .find_reference(reference)
        .context(format!("cannot find reference: {reference}"))?;
    if let Some(symref) = reference_obj.symbolic_target() {
        Ok(symref.to_string())
    } else {
        Ok(reference_obj
            .peel_to_commit()
            .context(format!("cannot get commit from reference: {reference}"))?
            .id()
            .to_string())
    }
}

fn get_refspecs_from_push_batch(stdin: &Stdin, initial_refspec: &str) -> Result<Vec<String>> {
    let mut line = String::new();
    let mut refspecs = vec![initial_refspec.to_string()];
    loop {
        let tokens = read_line(stdin, &mut line)?;
        match tokens.as_slice() {
            ["push", spec] => {
                refspecs.push((*spec).to_string());
            }
            [] => break,
            _ => {
                bail!("after a `push` command we are only expecting another push or an empty line")
            }
        }
    }
    Ok(refspecs)
}

trait BuildRepoState {
    async fn build(
        identifier: String,
        state: HashMap<String, String>,
        signer: &NostrSigner,
    ) -> Result<RepoState>;
}
impl BuildRepoState for RepoState {
    async fn build(
        identifier: String,
        state: HashMap<String, String>,
        signer: &NostrSigner,
    ) -> Result<RepoState> {
        let mut tags = vec![Tag::identifier(identifier.clone())];
        for (name, value) in &state {
            tags.push(Tag::custom(
                nostr_sdk_0_34_0::TagKind::Custom(name.into()),
                vec![value.clone()],
            ));
        }
        let event = sign_event(EventBuilder::new(STATE_KIND, "", tags), signer).await?;
        Ok(RepoState {
            identifier,
            state,
            event,
        })
    }
}

#[cfg(test)]
mod tests {
    use anyhow::Result as Result;
    use futures::join;
    use gnostr::test_utils::{
        generate_repo_ref_event_with_git_server, generate_test_key_1_metadata_event,
        generate_test_key_1_relay_list_event, get_proposal_branch_name_from_events,
    };
    use gnostr::test_utils::{
        git::GitTestRepo,
        git_remote::*,
        relay::Relay,
        {CliTester, FEATURE_BRANCH_NAME_1},
    };
    use gnostr::test_utils::E;
    use nostr_0_34_1::{Event, Kind};
    use std::collections::HashSet;

    mod refspec_to_from_to_tests {
        use crate::push::refspec_to_from_to;
        #[test]
        fn trailing_plus_stripped() {
            let (from, _) = refspec_to_from_to("+testing:testingb").unwrap();
            assert_eq!(from, "testing");
        }
    }

    mod integration_tests {
        use super::*;
        use serial_test::serial;
        use gnostr::test_utils::{git_remote::*, relay};
        use gnostr::client::STATE_KIND;
        use git2::Oid;
        use nostr_sdk_0_34_0::JsonUtil;

        #[tokio::test]
        #[serial]
        #[ignore]
        #[cfg(feature = "expensive_tests")]
        async fn new_branch_when_no_state_event_exists() -> Result<(), E> {
            generate_repo_with_state_event().await?;
            Ok(())
        }
        mod two_branches_in_batch_one_added_one_updated {
            use super::*;
            use anyhow::Context;
            use gnostr::test_utils::generate_repo_ref_event;
            use gnostr::test_utils::git_remote::*;
            use gnostr::test_utils::git_remote::STATE_KIND;
            use gnostr::test_utils::relay;


            #[tokio::test]
            #[serial]
            #[ignore]
            #[cfg(feature = "expensive_tests")]
            async fn updates_branch_on_git_server() -> Result<(), E> {
                let mut git_repo = prep_git_repo()?;
                let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo)?;

                std::fs::write(git_repo.dir.join("commit.md"), "some content")?;
                let main_commit_id = git_repo.stage_and_commit("commit.md")?;

                git_repo.create_branch("vnext")?;
                git_repo.checkout("vnext")?;
                std::fs::write(git_repo.dir.join("vnext.md"), "some content")?;
                let vnext_commit_id = git_repo.stage_and_commit("vnext.md")?;

                let events = vec![
                    generate_test_key_1_metadata_event("fred"),
                    generate_test_key_1_relay_list_event(),
                    generate_repo_ref_event_with_git_server(vec![source_git_repo
                        .dir
                        .to_str()
                        .unwrap()
                        .to_string()
                    ]),
                ];
                // fallback (51,52) user write (53, 55) repo (55, 56) blaster
                // (57)
                let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                    Relay::new(8051, None, None),
                    Relay::new(8052, None, None),
                    Relay::new(8053, None, None),
                    Relay::new(8055, None, None),
                    Relay::new(8056, None, None),
                    Relay::new(8057, None, None),
                );
                r51.events = events.clone();
                r55.events = events;

                let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                    assert_ne!(
                        source_git_repo.get_tip_of_local_branch("main")?,
                        main_commit_id
                    );

                    let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;

                    p.send_line("push refs/heads/main:refs/heads/main")?;
                    p.send_line("push refs/heads/vnext:refs/heads/vnext")?;
                    p.send_line("")?;
                    p.expect_eventually("\r\n\r\n")?;
                    p.exit()?;
                    for p in [51, 52, 53, 55, 56, 57] {
                        relay::shutdown_relay(8000 + p)?;
                    }

                    assert_eq!(
                        source_git_repo.get_tip_of_local_branch("main")?,
                        main_commit_id
                    );

                    assert_eq!(
                        source_git_repo.get_tip_of_local_branch("vnext")?,
                        vnext_commit_id
                    );

                    Ok(())
                });
                // launch relays
                let _ = join!(
                    r51.listen_until_close(),
                    r52.listen_until_close(),
                    r53.listen_until_close(),
                    r55.listen_until_close(),
                    r56.listen_until_close(),
                    r57.listen_until_close(),
                );
                cli_tester_handle.join().unwrap()?;
                Ok(())
            }

            #[tokio::test]
            #[serial]
            #[ignore]
            #[cfg(feature = "expensive_tests")]
            async fn remote_refs_updated_in_local_git() -> Result<(), E> {
                let mut git_repo = prep_git_repo()?;
                let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo)?;

                std::fs::write(git_repo.dir.join("commit.md"), "some content")?;
                let main_commit_id = git_repo.stage_and_commit("commit.md")?;

                git_repo.create_branch("vnext")?;
                git_repo.checkout("vnext")?;
                std::fs::write(git_repo.dir.join("vnext.md"), "some content")?;
                let vnext_commit_id = git_repo.stage_and_commit("vnext.md")?;

                let events = vec![
                    generate_test_key_1_metadata_event("fred"),
                    generate_test_key_1_relay_list_event(),
                    generate_repo_ref_event_with_git_server(vec![source_git_repo
                        .dir
                        .to_str()
                        .unwrap()
                        .to_string()
                    ]),
                ];
                // fallback (51,52) user write (53, 55) repo (55, 56) blaster
                // (57)
                let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                    Relay::new(8051, None, None),
                    Relay::new(8052, None, None),
                    Relay::new(8053, None, None),
                    Relay::new(8055, None, None),
                    Relay::new(8056, None, None),
                    Relay::new(8057, None, None),
                );
                r51.events = events.clone();
                r55.events = events;

                let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                    assert_ne!(
                        source_git_repo.get_tip_of_local_branch("main")?,
                        main_commit_id
                    );

                    let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
                    p.send_line("push refs/heads/main:refs/heads/main")?;
                    p.send_line("push refs/heads/vnext:refs/heads/vnext")?;
                    p.send_line("")?;
                    p.expect_eventually("\r\n\r\n")?;
                    p.exit()?;
                    for p in [51, 52, 53, 55, 56, 57] {
                        relay::shutdown_relay(8000 + p)?;
                    }

                    assert_eq!(
                        git_repo
                            .git_repo
                            .find_reference("refs/remotes/nostr/main")?
                            .peel_to_commit()? 
                            .id(),
                        main_commit_id,
                    );

                    assert_eq!(
                        git_repo
                            .git_repo
                            .find_reference("refs/remotes/nostr/vnext")?
                            .peel_to_commit()? 
                            .id(),
                        vnext_commit_id
                    );

                    p.exit()?;
                    for p in [51, 52, 53, 55, 56, 57] {
                        relay::shutdown_relay(8000 + p)?;
                    }
                    Ok(())
                });
                // launch relays
                let _ = join!(
                    r51.listen_until_close(),
                    r52.listen_until_close(),
                    r53.listen_until_close(),
                    r55.listen_until_close(),
                    r56.listen_until_close(),
                    r57.listen_until_close(),
                );
                cli_tester_handle.join().unwrap()?;
                Ok(())
            }

            #[tokio::test]
            #[serial]
            #[ignore]
            #[cfg(feature = "expensive_tests")]
                async fn prints_git_helper_ok_respose() -> Result<(), E> {
                let mut git_repo = prep_git_repo()?;
                let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo)?;

                std::fs::write(git_repo.dir.join("commit.md"), "some content")?;
                let main_commit_id = git_repo.stage_and_commit("commit.md")?;

                git_repo.create_branch("vnext")?;
                git_repo.checkout("vnext")?;
                std::fs::write(git_repo.dir.join("vnext.md"), "some content")?;
                git_repo.stage_and_commit("vnext.md")?;

                let events = vec![
                    generate_test_key_1_metadata_event("fred"),
                    generate_test_key_1_relay_list_event(),
                    generate_repo_ref_event_with_git_server(vec![source_git_repo
                        .dir
                        .to_str()
                        .unwrap()
                        .to_string()
                    ]),
                ];
                // fallback (51,52) user write (53, 55) repo (55, 56) blaster
                // (57)
                let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                    Relay::new(8051, None, None),
                    Relay::new(8052, None, None),
                    Relay::new(8053, None, None),
                    Relay::new(8055, None, None),
                    Relay::new(8056, None, None),
                    Relay::new(8057, None, None),
                );
                r51.events = events.clone();
                r55.events = events;

                let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                    assert_ne!(
                        source_git_repo.get_tip_of_local_branch("main")?,
                        main_commit_id
                    );

                    let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;

                    p.send_line("push refs/heads/main:refs/heads/main")?;
                    p.send_line("push refs/heads/vnext:refs/heads/vnext")?;
                    p.send_line("")?;
                    p.expect("ok refs/heads/main\r\n")?;
                    p.expect("ok refs/heads/vnext\r\n")?;
                    p.expect_eventually("\r\n\r\n")?;
                    p.exit()?;
                    for p in [51, 52, 53, 55, 56, 57] {
                        relay::shutdown_relay(8000 + p)?;
                    }
                    Ok(())
                });
                // launch relays
                let _ = join!(
                    r51.listen_until_close(),
                    r52.listen_until_close(),
                    r53.listen_until_close(),
                    r55.listen_until_close(),
                    r56.listen_until_close(),
                    r57.listen_until_close(),
                );
                cli_tester_handle.join().unwrap()?;
                Ok(())
            }

            #[tokio::test]
            #[serial]
            #[ignore]
            #[cfg(feature = "expensive_tests")]
            async fn when_no_existing_state_event_state_on_git_server_published_in_nostr_state_event() -> Result<(), E> {
                let mut git_repo = prep_git_repo()?;
                let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo)?;

                std::fs::write(git_repo.dir.join("commit.md"), "some content")?;
                let main_commit_id = git_repo.stage_and_commit("commit.md")?;

                git_repo.create_branch("vnext")?;
                git_repo.checkout("vnext")?;
                std::fs::write(git_repo.dir.join("vnext.md"), "some content")?;
                let vnext_commit_id = git_repo.stage_and_commit("vnext.md")?;

                let events = vec![
                    generate_test_key_1_metadata_event("fred"),
                    generate_test_key_1_relay_list_event(),
                    generate_repo_ref_event_with_git_server(vec![source_git_repo
                        .dir
                        .to_str()
                        .unwrap()
                        .to_string()
                    ]),
                ];
                // fallback (51,52) user write (53, 55) repo (55, 56) blaster
                // (57)
                let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                    Relay::new(8051, None, None),
                    Relay::new(8052, None, None),
                    Relay::new(8053, None, None),
                    Relay::new(8055, None, None),
                    Relay::new(8056, None, None),
                    Relay::new(8057, None, None),
                );
                r51.events = events.clone();
                r55.events = events;

                let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                    let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
                    p.send_line("push refs/heads/main:refs/heads/main")?;
                    p.send_line("push refs/heads/vnext:refs/heads/vnext")?;
                    p.send_line("")?;
                    p.expect_eventually_and_print("\r\n\r\n")?;
                    p.exit()?;
                    for p in [51, 52, 53, 55, 56, 57] {
                        relay::shutdown_relay(8000 + p)?;
                    }
                    Ok(())
                });
                // launch relays
                let _ = join!(
                    r51.listen_until_close(),
                    r52.listen_until_close(),
                    r53.listen_until_close(),
                    r55.listen_until_close(),
                    r56.listen_until_close(),
                    r57.listen_until_close(),
                );
                cli_tester_handle.join().unwrap()?;

                let state_event = r56
                    .events
                    .iter()
                    .find(|e| e.kind().eq(&STATE_KIND))
                    .context("state event not created")?;

                assert_eq!(
                    state_event.identifier(),
                    generate_repo_ref_event().identifier(),
                );
                // println!("{:#?}", state_event);
                assert_eq!(
                    state_event
                        .tags
                        .iter()
                        .filter(|t| t.kind().to_string().as_str().ne("d"))
                        .map(|t| t.as_vec().to_vec())
                        .collect::<HashSet<Vec<String>>>(),
                    HashSet::from([
                        vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()],
                        vec!["refs/heads/main".to_string(), main_commit_id.to_string()],
                        vec!["refs/heads/vnext".to_string(), vnext_commit_id.to_string()],
                    ]),
                );
                Ok(())
            }

            #[tokio::test]
            #[serial]
            #[ignore]
            #[cfg(feature = "expensive_tests")]
            async fn existing_state_event_published_in_nostr_state_event() -> Result<(), E> {
                let (state_event, source_git_repo) = generate_repo_with_state_event().await?;

                let mut git_repo = prep_git_repo()?;
                let example_branch_commit_id = git_repo.get_tip_of_local_branch("main")?.to_string(); // same as example

                std::fs::write(git_repo.dir.join("new.md"), "some content")?;
                let main_commit_id = git_repo.stage_and_commit("new.md")?;
                git_repo.create_branch("vnext")?;
                git_repo.checkout("vnext")?;
                std::fs::write(git_repo.dir.join("more.md"), "some content")?;
                let vnext_commit_id = git_repo.stage_and_commit("more.md")?;

                let events = vec![
                    generate_test_key_1_metadata_event("fred"),
                    generate_test_key_1_relay_list_event(),
                    generate_repo_ref_event_with_git_server(vec![source_git_repo
                        .dir
                        .to_str()
                        .unwrap()
                        .to_string()
                    ]),
                    state_event.clone(),
                ];

                // fallback (51,52) user write (53, 55) repo (55, 56) blaster
                // (57)
                let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                    Relay::new(8051, None, None),
                    Relay::new(8052, None, None),
                    Relay::new(8053, None, None),
                    Relay::new(8055, None, None),
                    Relay::new(8056, None, None),
                    Relay::new(8057, None, None),
                );
                r51.events = events.clone();
                r55.events = events;

                let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                    let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
                    p.send_line("push refs/heads/main:refs/heads/main")?;
                    p.send_line("push refs/heads/vnext:refs/heads/vnext")?;
                    p.send_line("")?;
                    p.expect_eventually_and_print("\r\n\r\n")?;
                    p.exit()?;
                    for p in [51, 52, 53, 55, 56, 57] {
                        relay::shutdown_relay(8000 + p)?;
                    }
                    // local refs updated
                    assert_eq!(
                        git_repo
                            .git_repo
                            .find_reference("refs/remotes/nostr/main")?
                            .peel_to_commit()? 
                            .id(),
                        main_commit_id,
                    );

                    assert_eq!(
                        git_repo
                            .git_repo
                            .find_reference("refs/remotes/nostr/vnext")?
                            .peel_to_commit()? 
                            .id(),
                        vnext_commit_id
                    );
                    Ok(())
                });
                // launch relays
                let _ = join!(
                    r51.listen_until_close(),
                    r52.listen_until_close(),
                    r53.listen_until_close(),
                    r55.listen_until_close(),
                    r56.listen_until_close(),
                    r57.listen_until_close(),
                );

                cli_tester_handle.join().unwrap()?;

                // git_server updated
                assert_eq!(
                    source_git_repo.get_tip_of_local_branch("main")?,
                    main_commit_id
                );

                assert_eq!(
                    source_git_repo.get_tip_of_local_branch("vnext")?,
                    vnext_commit_id
                );

                // state annoucement updated
                let state_event = r56
                    .events
                    .iter()
                    .find(|e| e.kind().eq(&STATE_KIND))
                    .context("state event not created")?;

                // println!("{:#?}", state_event);
                assert_eq!(
                    state_event
                        .tags
                        .iter()
                        .filter(|t| t.kind().to_string().as_str().ne("d"))
                        .map(|t| t.as_vec().to_vec())
                        .collect::<HashSet<Vec<String>>>(),
                    HashSet::from([
                        vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()],
                        vec!["refs/heads/main".to_string(), main_commit_id.to_string()],
                        vec![
                            "refs/heads/example-branch".to_string(),
                            example_branch_commit_id.to_string(),
                        ],
                        vec!["refs/heads/vnext".to_string(), vnext_commit_id.to_string()],
                    ]),
                );
                Ok(())
            }
        }
        mod delete_one_branch {
            use super::*;
            use gnostr::test_utils::git_remote::*;
            use gnostr::test_utils::relay;


            #[tokio::test]
            #[serial]
            #[ignore]
            #[cfg(feature = "expensive_tests")]
            async fn deletes_branch_on_git_server() -> Result<(), E> {
                let mut git_repo = prep_git_repo()?;

                git_repo.create_branch("vnext")?;
                git_repo.checkout("vnext")?;
                std::fs::write(git_repo.dir.join("vnext.md"), "some content")?;
                let vnext_commit_id = git_repo.stage_and_commit("vnext.md")?;

                let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo)?;

                let events = vec![
                    generate_test_key_1_metadata_event("fred"),
                    generate_test_key_1_relay_list_event(),
                    generate_repo_ref_event_with_git_server(vec![source_git_repo
                        .dir
                        .to_str()
                        .unwrap()
                        .to_string()
                    ]),
                ];
                // fallback (51,52) user write (53, 55) repo (55, 56) blaster
                // (57)
                let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                    Relay::new(8051, None, None),
                    Relay::new(8052, None, None),
                    Relay::new(8053, None, None),
                    Relay::new(8055, None, None),
                    Relay::new(8056, None, None),
                    Relay::new(8057, None, None),
                );
                r51.events = events.clone();
                r55.events = events;

                let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                    assert_eq!(
                        source_git_repo
                            .git_repo
                            .find_reference("refs/heads/vnext")?.target().unwrap(),
                        vnext_commit_id
                    );

                    let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
                    p.send_line("push :refs/heads/vnext")?;
                    p.send_line("")?;
                    p.expect_eventually_and_print("\r\n\r\n")?;
                    p.exit()?;
                    for p in [51, 52, 53, 55, 56, 57] {
                        relay::shutdown_relay(8000 + p)?;
                    }

                    assert!(source_git_repo
                        .git_repo
                        .find_reference("refs/heads/vnext")
                        .is_err());
                    Ok(())
                });
                // launch relays
                let _ = join!(
                    r51.listen_until_close(),
                    r52.listen_until_close(),
                    r53.listen_until_close(),
                    r55.listen_until_close(),
                    r56.listen_until_close(),
                    r57.listen_until_close(),
                );
                cli_tester_handle.join().unwrap()?;
                Ok(())
            }

            #[tokio::test]
            #[serial]
            #[ignore]
            #[cfg(feature = "expensive_tests")]
            async fn remote_refs_updated_in_local_git() -> Result<(), E> {
                let mut git_repo = prep_git_repo()?;

                git_repo.create_branch("vnext")?;
                git_repo.checkout("vnext")?;
                std::fs::write(git_repo.dir.join("vnext.md"), "some content")?;
                let vnext_commit_id = git_repo.stage_and_commit("vnext.md")?;

                let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo)?;

                git_repo
                    .git_repo
                    .reference("refs/remotes/nostr/vnext", vnext_commit_id, true, "")?;

                let events = vec![
                    generate_test_key_1_metadata_event("fred"),
                    generate_test_key_1_relay_list_event(),
                    generate_repo_ref_event_with_git_server(vec![source_git_repo
                        .dir
                        .to_str()
                        .unwrap()
                        .to_string()
                    ]),
                ];
                // fallback (51,52) user write (53, 55) repo (55, 56) blaster
                // (57)
                let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                    Relay::new(8051, None, None),
                    Relay::new(8052, None, None),
                    Relay::new(8053, None, None),
                    Relay::new(8055, None, None),
                    Relay::new(8056, None, None),
                    Relay::new(8057, None, None),
                );
                r51.events = events.clone();
                r55.events = events;

                let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                    assert_eq!(
                        git_repo
                            .git_repo
                            .find_reference("refs/remotes/nostr/vnext")?.target().unwrap(),
                        vnext_commit_id
                    );

                    let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
                    p.send_line("push :refs/heads/vnext")?;
                    p.send_line("")?;
                    p.expect_eventually("\r\n\r\n")?;
                    p.exit()?;
                    for p in [51, 52, 53, 55, 56, 57] {
                        relay::shutdown_relay(8000 + p)?;
                    }
                    assert!(git_repo
                        .git_repo
                        .find_reference("refs/remotes/nostr/vnext")
                        .is_err());
                    Ok(())
                });
                // launch relays
                let _ = join!(
                    r51.listen_until_close(),
                    r52.listen_until_close(),
                    r53.listen_until_close(),
                    r55.listen_until_close(),
                    r56.listen_until_close(),
                    r57.listen_until_close(),
                );
                cli_tester_handle.join().unwrap()?;
                Ok(())
            }

            #[tokio::test]
            #[serial]
            #[ignore]
            #[cfg(feature = "expensive_tests")]
            async fn prints_git_helper_ok_respose() -> Result<(), E> {
                let mut git_repo = prep_git_repo()?;

                git_repo.create_branch("vnext")?;
                git_repo.checkout("vnext")?;
                std::fs::write(git_repo.dir.join("vnext.md"), "some content")?;
                let vnext_commit_id = git_repo.stage_and_commit("vnext.md")?;

                let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo)?;

                git_repo
                    .git_repo
                    .reference("refs/remotes/nostr/vnext", vnext_commit_id, true, "")?;

                let events = vec![
                    generate_test_key_1_metadata_event("fred"),
                    generate_test_key_1_relay_list_event(),
                    generate_repo_ref_event_with_git_server(vec![source_git_repo
                        .dir
                        .to_str()
                        .unwrap()
                        .to_string()
                    ]),
                ];
                // fallback (51,52) user write (53, 55) repo (55, 56) blaster
                // (57)
                let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                    Relay::new(8051, None, None),
                    Relay::new(8052, None, None),
                    Relay::new(8053, None, None),
                    Relay::new(8055, None, None),
                    Relay::new(8056, None, None),
                    Relay::new(8057, None, None),
                );
                r51.events = events.clone();
                r55.events = events;

                let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                    let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
                    p.send_line("push :refs/heads/vnext")?;
                    p.send_line("")?;
                    p.expect("ok refs/heads/vnext\r\n")?;
                    p.expect_eventually("\r\n\r\n")?;
                    p.exit()?;
                    for p in [51, 52, 53, 55, 56, 57] {
                        relay::shutdown_relay(8000 + p)?;
                    }
                    Ok(())
                });
                // launch relays
                let _ = join!(
                    r51.listen_until_close(),
                    r52.listen_until_close(),
                    r53.listen_until_close(),
                    r55.listen_until_close(),
                    r56.listen_until_close(),
                    r57.listen_until_close(),
                );
                cli_tester_handle.join().unwrap()?;
                Ok(())
            }

            mod when_existing_state_event {
                use super::*;
                use gnostr::client::STATE_KIND;


                use anyhow::Context;

                #[tokio::test]
                #[serial]
                #[cfg(feature = "expensive_tests")]
                #[ignore]
                async fn state_event_updated_and_branch_deleted_and_ok_printed() -> Result<(), E> {
                    let (state_event, source_git_repo) = generate_repo_with_state_event().await?;

                    let mut git_repo = prep_git_repo()?;
                    let main_commit_id = git_repo.get_tip_of_local_branch("main")?.to_string(); // same as example

                    let events = vec![
                        generate_test_key_1_metadata_event("fred"),
                        generate_test_key_1_relay_list_event(),
                        generate_repo_ref_event_with_git_server(vec![source_git_repo
                            .dir
                            .to_str()
                            .unwrap()
                            .to_string()
                        ]),
                        state_event.clone(),
                    ];

                    // fallback (51,52) user write (53, 55) repo (55, 56)
                    // blaster (57)
                    let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                        Relay::new(8051, None, None),
                        Relay::new(8052, None, None),
                        Relay::new(8053, None, None),
                        Relay::new(8055, None, None),
                        Relay::new(8056, None, None),
                        Relay::new(8057, None, None),
                    );
                    r51.events = events.clone();
                    r55.events = events;

                    let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                        let mut p =
                            cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
                        p.send_line("push :refs/heads/example-branch")?;
                        p.send_line("")?;
                        p.expect("ok refs/heads/example-branch\r\n")?;
                        p.expect_eventually("\r\n\r\n")?;
                        p.exit()?;
                        for p in [51, 52, 53, 55, 56, 57] {
                            relay::shutdown_relay(8000 + p)?;
                        }
                        Ok(())
                    });
                    // launch relays
                    let _ = join!(
                        r51.listen_until_close(),
                        r52.listen_until_close(),
                        r53.listen_until_close(),
                        r55.listen_until_close(),
                        r56.listen_until_close(),
                        r57.listen_until_close(),
                    );

                    cli_tester_handle.join().unwrap()?;

                    let state_event = r56
                        .events
                        .iter()
                        .find(|e| e.kind().eq(&STATE_KIND))
                        .context("state event not created")?;

                    // println!("{:#?}", state_event);
                    assert_eq!(
                        state_event
                            .tags
                            .iter()
                            .filter(|t| t.kind().to_string().as_str().ne("d"))
                            .map(|t| t.as_vec().to_vec())
                            .collect::<HashSet<Vec<String>>>(),
                        HashSet::from([
                            vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()],
                            vec!["refs/heads/main".to_string(), main_commit_id.to_string()],
                        ]),
                    );
                    Ok(())
                }

                mod already_deleted_on_git_server {


                    use anyhow::Context;
                    use futures::join;
                    use gnostr::test_utils::generate_repo_ref_event_with_git_server;
                    use gnostr::test_utils::git_remote::prep_git_repo;
                    use gnostr::test_utils::git_remote::cli_tester_after_nostr_fetch_and_sent_list_for_push_responds;
                    use gnostr::test_utils::generate_test_key_1_relay_list_event;
                    use gnostr::test_utils::git_remote::STATE_KIND;
                    use gnostr::test_utils::E;
                    use gnostr::test_utils::git::GitTestRepo;
                    use gnostr::test_utils::relay;
                    use gnostr::test_utils::relay::Relay;
                    use gnostr::test_utils::git_remote::generate_repo_with_state_event;
                    use gnostr::test_utils::generate_test_key_1_metadata_event;
                    use serial_test::serial;
                    use std::collections::HashSet;

                    #[tokio::test]
                    #[serial]
                    #[ignore]
                    #[cfg(feature = "expensive_tests")]
                    async fn existing_state_event_updated_and_ok_printed() -> Result<(), E> {
                        let (state_event, source_git_repo) = generate_repo_with_state_event().await?;

                        {
                            // delete branch on git server
                            let tmp_repo = GitTestRepo::clone_repo(&source_git_repo)?;
                            let mut remote = tmp_repo.git_repo.find_remote("origin")?;
                            remote.push(&[":refs/heads/example-branch"], None)?;
                        }

                        let git_repo = prep_git_repo()?;
                        let main_commit_id = git_repo.get_tip_of_local_branch("main")?.to_string(); // same as example

                        let events = vec![
                            generate_test_key_1_metadata_event("fred"),
                            generate_test_key_1_relay_list_event(),
                            generate_repo_ref_event_with_git_server(vec![source_git_repo
                                .dir
                                .to_str()
                                .unwrap()
                                .to_string()
                            ]),
                            state_event.clone(),
                        ];

                        // fallback (51,52) user write (53, 55) repo (55, 56)
                        // blaster (57)
                        let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                            Relay::new(8051, None, None),
                            Relay::new(8052, None, None),
                            Relay::new(8053, None, None),
                            Relay::new(8055, None, None),
                            Relay::new(8056, None, None),
                            Relay::new(8057, None, None),
                        );
                        r51.events = events.clone();
                        r55.events = events;

                        let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                            let mut p =
                                cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
                            p.send_line("push :refs/heads/example-branch")?;
                            p.send_line("")?;
                            p.expect("ok refs/heads/example-branch\r\n")?;
                            p.expect_eventually("\r\n")?;
                            p.exit()?;
                            for p in [51, 52, 53, 55, 56, 57] {
                                relay::shutdown_relay(8000 + p)?;
                            }
                            Ok(())
                        });
                        // launch relays
                        let _ = join!(
                            r51.listen_until_close(),
                            r52.listen_until_close(),
                            r53.listen_until_close(),
                            r55.listen_until_close(),
                            r56.listen_until_close(),
                            r57.listen_until_close(),
                        );

                        cli_tester_handle.join().unwrap()?;

                        let state_event = r56
                            .events
                            .iter()
                            .find(|e| e.kind().eq(&STATE_KIND))
                            .context("state event not created")?;

                        // println!("{:#?}", state_event);
                        assert_eq!(
                            state_event
                                .tags
                                .iter()
                                .filter(|t| t.kind().to_string().as_str().ne("d"))
                                .map(|t| t.as_vec().to_vec())
                                .collect::<HashSet<Vec<String>>>(),
                            HashSet::from([
                                vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()],
                                vec!["refs/heads/main".to_string(), main_commit_id.to_string()],
                            ]),
                        );
                        Ok(())
                    }
                }
            }
        }

        #[tokio::test]
        #[serial]
        #[ignore]
        #[cfg(feature = "expensive_tests")]
        async fn pushes_to_all_git_servers_listed_and_ok_printed() -> Result<(), E> {
            let (state_event, source_git_repo) = generate_repo_with_state_event().await?;
            let second_source_git_repo = GitTestRepo::duplicate(&source_git_repo)?;

            // uppdate announcement with extra git server

            let mut git_repo = prep_git_repo()?;

            std::fs::write(git_repo.dir.join("new.md"), "some content")?;
            let main_commit_id = git_repo.stage_and_commit("new.md")?;

            let events = vec![
                generate_test_key_1_metadata_event("fred"),
                generate_test_key_1_relay_list_event(),
                generate_repo_ref_event_with_git_server(vec![
                    source_git_repo.dir.to_str().unwrap().to_string(),
                    second_source_git_repo.dir.to_str().unwrap().to_string(),
                ]),
                state_event.clone(),
            ];

            // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57)
            let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                Relay::new(8051, None, None),
                Relay::new(8052, None, None),
                Relay::new(8053, None, None),
                Relay::new(8055, None, None),
                Relay::new(8056, None, None),
                Relay::new(8057, None, None),
            );
            r51.events = events.clone();
            r55.events = events;

            let cli_tester_handle = std::thread::spawn(move || -> Result<(), E> {
                let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
                p.send_line("push refs/heads/main:refs/heads/main")?;
                p.send_line("")?;
                p.expect("ok refs/heads/main\r\n")?;
                p.expect_eventually("\r\n\r\r\n")?;
                p.exit()?;
                for p in [51, 52, 53, 55, 56, 57] {
                    relay::shutdown_relay(8000 + p)?;
                }
                Ok(())
            });
            // launch relays
            let _ = join!(
                r51.listen_until_close(),
                r52.listen_until_close(),
                r53.listen_until_close(),
                r55.listen_until_close(),
                r56.listen_until_close(),
                r57.listen_until_close(),
            );

            cli_tester_handle.join().unwrap()?;

            // git_server updated
            assert_eq!(
                source_git_repo.get_tip_of_local_branch("main")?,
                main_commit_id
            );
            assert_eq!(
                second_source_git_repo.get_tip_of_local_branch("main")?,
                main_commit_id
            );

            Ok(())
        }

        #[tokio::test]
        #[serial]
        #[ignore]
        #[cfg(feature = "expensive_tests")]
        async fn proposal_merge_commit_pushed_to_main_leads_to_status_event_issued() -> Result<(), E> {
            //
            let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
            let source_path = source_git_repo.dir.to_str().unwrap().to_string();

            let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                Relay::new(8051, None, None),
                Relay::new(8052, None, None),
                Relay::new(8053, None, None),
                Relay::new(8055, None, None),
                Relay::new(8056, None, None),
                Relay::new(8057, None, None),
            );
            r51.events = events.clone();
            r55.events = events.clone();

            #[allow(clippy::mutable_key_type)]
            let before = r55.events.iter().cloned().collect::<HashSet<Event>>();

            let cli_tester_handle = std::thread::spawn(move || -> Result<(String, Oid), E> {
                let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;

                let mut git_repo = clone_git_repo_with_nostr_url()?;
                git_repo.checkout_remote_branch(&branch_name)?;
                git_repo.checkout("refs/heads/main")?;

                std::fs::write(git_repo.dir.join("new.md"), "some content")?;
                git_repo.stage_and_commit("new.md")?;

                CliTester::new_git_with_remote_helper_from_dir(
                    &git_repo.dir,
                    ["merge", &branch_name, "-m", "proposal merge commit message"],
                )
                .expect_end_eventually_and_print()?;

                let oid = git_repo.get_tip_of_local_branch("main")?;

                let mut p = CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push"]);
                cli_expect_nostr_fetch(&mut p)?;
                p.expect(format!("fetching {} ref list over filesystem...\r\n", source_path).as_str())?;
                p.expect("list: connecting...\r\n")?;
                p.expect_after_whitespace("merge commit ")?;
                // shorthand merge commit id appears in this gap
                p.expect_eventually(": create nostr proposal status event\r\n")?;
                // status updates printed here
                p.expect_eventually(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
                let output = p.expect_end_eventually()?;

                for p in [51, 52, 53, 55, 56, 57] {
                    relay::shutdown_relay(8000 + p)?;
                }

                Ok((output, oid))
            });
            // launch relays
            let _ = join!(
                r51.listen_until_close(),
                r52.listen_until_close(),
                r53.listen_until_close(),
                r55.listen_until_close(),
                r56.listen_until_close(),
                r57.listen_until_close(),
            );

            let (output, oid) = cli_tester_handle.join().unwrap()?;

            assert_eq!(
                output,
                format!("   431b84e..{}  main -> main\r\n", &oid.to_string()[..7])
            );

            let new_events = r55
                .events
                .iter()
                .cloned()
                .collect::<HashSet<Event>>()
                .difference(&before)
                .cloned()
                .collect::<Vec<Event>>();

            assert_eq!(new_events.len(), 2, "{new_events:?}");

            let proposal = r55
                .events
                .iter()
                .find(|e| {
                    e.tags()
                        .iter()
                        .find(|t| t.as_vec()[0].eq("branch-name"))
                        .is_some_and(|t| t.as_vec()[1].eq(FEATURE_BRANCH_NAME_1))
                })
                .unwrap();

            let merge_status = new_events
                .iter()
                .find(|e| e.kind().eq(&Kind::GitStatusApplied))
                .unwrap();

            assert_eq!(
                oid.to_string(),
                merge_status
                    .tags
                    .iter()
                    .find(|t| t.as_vec()[0].eq("merge-commit-id"))
                    .unwrap()
                    .as_vec()[1],
                "status sets correct merge-commit-id tag"
            );

            let proposal_tip = r55
                .events
                .iter()
                .filter(|e| {
                    e.tags()
                        .iter()
                        .any(|t| t.as_vec()[1].eq(&proposal.id().to_string()))
                        && e.kind().eq(&Kind::GitPatch)
                })
                .last()
                .unwrap();

            assert_eq!(
                proposal_tip.id().to_string(),
                merge_status
                    .tags
                    .iter()
                    .find(|t| t.as_vec().len().eq(&4) && t.as_vec()[3].eq("mention"))
                    .unwrap()
                    .as_vec()[1],
                "status mentions proposal tip event \r\nmerge status:\r\n{} {} {}",
                merge_status.as_json(),
                "proposal tip:\r\n{}",
                proposal_tip.as_json(),
            );

            assert_eq!(
                proposal.id().to_string(),
                merge_status
                    .tags
                    .iter()
                    .find(|t| t.is_root())
                    .unwrap()
                    .as_vec()[1],
                "status tags proposal id as root \r\nmerge status:\r\n{} {} {}",
                merge_status.as_json(),
                "proposal:\r\n{}",
                proposal.as_json(),
            );

            Ok(())
        }

        #[tokio::test]
        #[serial]
        #[ignore]
        #[cfg(feature = "expensive_tests")]
            async fn push_2_commits_to_existing_proposal() -> Result<(), E> {
            let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
            let source_path = source_git_repo.dir.to_str().unwrap().to_string();

            let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                Relay::new(8051, None, None),
                Relay::new(8052, None, None),
                Relay::new(8053, None, None),
                Relay::new(8055, None, None),
                Relay::new(8056, None, None),
                Relay::new(8057, None, None),
            );
            r51.events = events.clone();
            r55.events = events.clone();

            #[allow(clippy::mutable_key_type)]
            let before = r55.events.iter().cloned().collect::<HashSet<Event>>();

            let cli_tester_handle = std::thread::spawn(move || -> Result<(String, String), E> {
                let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;

                let mut git_repo = clone_git_repo_with_nostr_url()?;
                git_repo.checkout_remote_branch(&branch_name)?;

                std::fs::write(git_repo.dir.join("new.md"), "some content")?;
                git_repo.stage_and_commit("new.md")?;

                std::fs::write(git_repo.dir.join("new2.md"), "some content")?;
                git_repo.stage_and_commit("new2.md")?;

                let mut p = CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push"]);
                cli_expect_nostr_fetch(&mut p)?;
                p.expect(format!("fetching {} ref list over filesystem...\r\n", source_path).as_str())?;
                p.expect("list: connecting...\r\n\r\r\r")?;
                p.expect(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
                let output = p.expect_end_eventually()?;

                for p in [51, 52, 53, 55, 56, 57] {
                    relay::shutdown_relay(8000 + p)?;
                }

                Ok((output, branch_name))
            });
            // launch relays
            let _ = join!(
                r51.listen_until_close(),
                r52.listen_until_close(),
                r53.listen_until_close(),
                r55.listen_until_close(),
                r56.listen_until_close(),
                r57.listen_until_close(),
            );

            let (output, branch_name) = cli_tester_handle.join().unwrap()?;

            assert_eq!(
                output,
                format!("   eb5d678..7de5e41  {branch_name} -> {branch_name}\r\n").as_str(),
            );

            let new_events = r55
                .events
                .iter()
                .cloned()
                .collect::<HashSet<Event>>()
                .difference(&before)
                .cloned()
                .collect::<Vec<Event>>();
            assert_eq!(new_events.len(), 2);
            let first_new_patch = new_events
                .iter()
                .find(|e| e.content.contains("new.md"))
                .unwrap();
            let second_new_patch = new_events
                .iter()
                .find(|e| e.content.contains("new2.md"))
                .unwrap();
            assert!(
                first_new_patch.content.contains("[PATCH 3/4]"),
                "first patch labeled with         [PATCH 3/4]"
            );
            assert!(
                second_new_patch.content.contains("[PATCH 4/4]"),
                "second patch labeled with         [PATCH 4/4]"
            );

            let proposal = r55
                .events
                .iter()
                .find(|e| {
                    e.tags()
                        .iter()
                        .find(|t| t.as_vec()[0].eq("branch-name"))
                        .is_some_and(|t| t.as_vec()[1].eq(FEATURE_BRANCH_NAME_1))
                })
                .unwrap();

            assert_eq!(
                proposal.id().to_string(),
                first_new_patch
                    .tags
                    .iter()
                    .find(|t| t.is_root())
                    .unwrap()
                    .as_vec()[1],
                "first patch sets proposal id as root"
            );

            assert_eq!(
                first_new_patch.id().to_string(),
                second_new_patch
                    .tags
                    .iter()
                    .find(|t| t.is_reply())
                    .unwrap()
                    .as_vec()[1],
                "second new patch replies to the first new patch"
            );

            let previous_proposal_tip_event = r55
                .events
                .iter()
                .find(|e| {
                    e.tags()
                        .iter()
                        .any(|t| t.as_vec()[1].eq(&proposal.id().to_string()))
                        && e.content.contains("[PATCH 2/2]")
                })
                .unwrap();

            assert_eq!(
                previous_proposal_tip_event.id().to_string(),
                first_new_patch
                    .tags
                    .iter()
                    .find(|t| t.is_reply())
                    .unwrap()
                    .as_vec()[1],
                "first patch replies to the previous tip of proposal"
            );

            Ok(())
        }

        #[tokio::test]
        #[serial]
        #[ignore]
        #[cfg(feature = "expensive_tests")]
        async fn force_push_creates_proposal_revision() -> Result<(), E> {
            let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
            let source_path = source_git_repo.dir.to_str().unwrap().to_string();

            let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                Relay::new(8051, None, None),
                Relay::new(8052, None, None),
                Relay::new(8053, None, None),
                Relay::new(8055, None, None),
                Relay::new(8056, None, None),
                Relay::new(8057, None, None),
            );
            r51.events = events.clone();
            r55.events = events.clone();

            #[allow(clippy::mutable_key_type)]
            let before = r55.events.iter().cloned().collect::<HashSet<Event>>();

            let cli_tester_handle = std::thread::spawn(move || -> Result<(String, String), E> {
                let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;

                let mut git_repo = clone_git_repo_with_nostr_url()?;
                let oid = git_repo.checkout_remote_branch(&branch_name)?;
                // remove last commit
                git_repo.checkout("main")?;
                git_repo.git_repo.branch(
                    &branch_name,
                    &git_repo.git_repo.find_commit(oid)?.parent(0)?,
                    true,
                )?;
                git_repo.checkout(&branch_name)?;

                std::fs::write(git_repo.dir.join("new.md"), "some content")?;
                git_repo.stage_and_commit("new.md")?;

                std::fs::write(git_repo.dir.join("new2.md"), "some content")?;
                git_repo.stage_and_commit("new2.md")?;

                let mut p =
                    CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push", "--force"]);
                cli_expect_nostr_fetch(&mut p)?;
                p.expect(format!("fetching {} ref list over filesystem...\r\n", source_path).as_str())?;
                p.expect("list: connecting...\r\n")?;
                p.expect_after_whitespace(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
                let output = p.expect_end_eventually()?;

                for p in [51, 52, 53, 55, 56, 57] {
                    relay::shutdown_relay(8000 + p)?;
                }

                Ok((output, branch_name))
            });
            // launch relays
            let _ = join!(
                r51.listen_until_close(),
                r52.listen_until_close(),
                r53.listen_until_close(),
                r55.listen_until_close(),
                r56.listen_until_close(),
                r57.listen_until_close(),
            );

            let (output, branch_name) = cli_tester_handle.join().unwrap()?;

            assert_eq!(
                output,
                format!(" + eb5d678...8a296c8 {branch_name} -> {branch_name} (forced update)\r\n").as_str(),
            );

            let new_events = r55
                .events
                .iter()
                .cloned()
                .collect::<HashSet<Event>>()
                .difference(&before)
                .cloned()
                .collect::<Vec<Event>>();
            assert_eq!(new_events.len(), 3);

            let proposal = r55
                .events
                .iter()
                .find(|e| {
                    e.tags()
                        .iter()
                        .find(|t| t.as_vec()[0].eq("branch-name"))
                        .is_some_and(|t| t.as_vec()[1].eq(FEATURE_BRANCH_NAME_1))
                })
                .unwrap();

            let revision_root_patch = new_events
                .iter()
                .find(|e| e.tags().iter().any(|t| t.as_vec()[1].eq("revision-root")))
                .unwrap();

            assert_eq!(
                proposal.id().to_string(),
                revision_root_patch
                    .tags
                    .iter()
                    .find(|t| t.is_reply())
                    .unwrap()
                    .as_vec()[1],
                "revision root patch replies to original proposal"
            );

            assert!(
                revision_root_patch.content.contains("[PATCH 1/3]"),
                "revision root labeled with    [PATCH 1/3] event: {revision_root_patch:?}",
            );

            let second_patch = new_events
                .iter()
                .find(|e| e.content.contains("new.md"))
                .unwrap();
            let third_patch = new_events
                .iter()
                .find(|e| e.content.contains("new2.md"))
                .unwrap();
            assert!(
                second_patch.content.contains("[PATCH 2/3]"),
                "second patch labeled with     [PATCH 2/3]"
            );
            assert!(
                third_patch.content.contains("[PATCH 3/3]"),
                "third patch labeled with     [PATCH 3/3]"
            );

            assert_eq!(
                revision_root_patch.id().to_string(),
                second_patch
                    .tags
                    .iter()
                    .find(|t| t.is_root())
                    .unwrap()
                    .as_vec()[1],
                "second patch sets revision id as root"
            );

            assert_eq!(
                second_patch.id().to_string(),
                third_patch
                    .tags
                    .iter()
                    .find(|t| t.is_reply())
                    .unwrap()
                    .as_vec()[1],
                "third patch replies to the second new patch"
            );

            Ok(())
        }

        #[tokio::test]
        #[serial]
        #[ignore]
        #[cfg(feature = "expensive_tests")]
            async fn push_new_pr_branch_creates_proposal() -> Result<(), E> {
            let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
            let source_path = source_git_repo.dir.to_str().unwrap().to_string();

            let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
                Relay::new(8051, None, None),
                Relay::new(8052, None, None),
                Relay::new(8053, None, None),
                Relay::new(8055, None, None),
                Relay::new(8056, None, None),
                Relay::new(8057, None, None),
            );
            r51.events = events.clone();
            r55.events = events.clone();

            #[allow(clippy::mutable_key_type)]
            let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
            let branch_name = "pr/my-new-proposal";

            let cli_tester_handle = std::thread::spawn(move || -> Result<String, E> {
                let mut git_repo = clone_git_repo_with_nostr_url()?;
                git_repo.delete_dir_on_drop = false;
                git_repo.create_branch(branch_name)?;
                git_repo.checkout(branch_name)?;

                std::fs::write(git_repo.dir.join("new.md"), "some content")?;
                git_repo.stage_and_commit("new.md")?;

                std::fs::write(git_repo.dir.join("new2.md"), "some content")?;
                git_repo.stage_and_commit("new2.md")?;

                let mut p = CliTester::new_git_with_remote_helper_from_dir(
                    &git_repo.dir,
                    ["push", "-u", "origin", branch_name],
                );
                cli_expect_nostr_fetch(&mut p)?;
                p.expect(format!("fetching {} ref list over filesystem...\r\n", source_path).as_str())?;
                p.expect("list: connecting...\r\n\r\r\r")?;
                p.expect(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
                let output = p.expect_end_eventually()?;

                for p in [51, 52, 53, 55, 56, 57] {
                    relay::shutdown_relay(8000 + p)?;
                }

                Ok(output)
            });
            // launch relays
            let _ = join!(
                r51.listen_until_close(),
                r52.listen_until_close(),
                r53.listen_until_close(),
                r55.listen_until_close(),
                r56.listen_until_close(),
                r57.listen_until_close(),
            );

            let output = cli_tester_handle.join().unwrap()?;

            assert_eq!(
                    output,
                    format!(" * [new branch]      {branch_name} -> {branch_name}\r\nbranch '{branch_name}' set up to track 'origin/{branch_name}'.\r\n").as_str(),
                );

            let new_events = r55
                .events
                .iter()
                .cloned()
                .collect::<HashSet<Event>>()
                .difference(&before)
                .cloned().collect::<Vec<Event>>()
                .into_iter().collect::<Vec<Event>>();
            assert_eq!(new_events.len(), 2);

            let proposal = new_events
                .iter()
                .find(|e| e.tags().iter().any(|t| t.as_vec()[1].eq("root")))
                .unwrap();

            assert!(
                proposal.content.contains("new.md"),
                "first patch is proposal root"
            );

            assert!(
                proposal.content.contains("[PATCH 1/2]"),
                "proposal root labeled with[PATCH 1/2] event: {proposal:?}"
            );

            assert_eq!(
                proposal
                    .tags()
                    .iter()
                    .find(|t| t.as_vec()[0].eq("branch-name"))
                    .unwrap()
                    .as_vec()[1],
                branch_name.replace("pr/", ""),
            );

            let second_patch = new_events
                .iter()
                .find(|e| e.content.contains("new2.md"))
                .unwrap();

            assert!(
                second_patch.content.contains("[PATCH 2/2]"),
                "second patch labeled with     [PATCH 2/2]"
            );

            assert_eq!(
                proposal.id().to_string(),
                second_patch
                    .tags
                    .iter()
                    .find(|t| t.is_root())
                    .unwrap()
                    .as_vec()[1],
                "second patch sets proposal id as root"
            );

            Ok(())
        }
    }
}
